/*
    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 commands.cpp The basic definitions or arcs that represent commands.
 * @ingroup libmscgen_files */

#define _CRT_SECURE_NO_DEPRECATE //For visual studio to allow sscanf
#include <algorithm>
#include "msc.h"
#include "cgencommon.h"

using namespace std;
using namespace msc;
using contour::deg2rad;
using contour::rad2deg;

void Command::Layout(Canvas &/*canvas*/, AreaList * /*cover*/)
{
    height = 0;
    if (!valid) return;
    Block b(chart->GetDrawing().x.from, chart->GetDrawing().x.till,
            -chart->nudgeSize/2, chart->nudgeSize/2);
    area_draw = b;
    draw_is_different = true; //area is empty - never find this
    _ASSERT(comments.size()==0);
}


//////////////////////////////////////////////////////////////////////////////////////

EntityCommand::EntityCommand(gsl::owner<EntityAppHelper*> e, MscChart *msc, bool in)
    : Command(EArcCategory::ENTITY, msc),
    internally_defined(in)
    //tmp_stored_notes is responsible for its content - if the EntityCommand is
    //destroyed during parse, these notes must also get deleted
{
    _ASSERT(e);
    if (e) {
        entities.splice(entities.end(), e->entities);
        std::ranges::move(e->notes, std::back_inserter(tmp_stored_notes));
        target_entity = std::move(e->target);
        delete e;
    }
}

/** This constructor is used at a single 'show' 'hide' 'activate' 'deactivate'
 * keyword. Will include *all* entities defined _so far_ with the prefix applied.*/
EntityCommand::EntityCommand(MscChart* chart, std::string_view command, const FileLineColRange & l)
    : Command(EArcCategory::ENTITY, chart),
    internally_defined(false)
    //tmp_stored_notes is responsible for its content - if the EntityCommand is
    //destroyed during parse, these notes must also get deleted
{
    //Add a multi-entityApp.
    bool showing = true;
    //adjust showing and command
    if (CaseInsensitiveEqual(command, "heading")) command = "show";
    else if (CaseInsensitiveEqual(command, "show")) showing = false; //show the ones not showing now
    entities.Append(std::make_unique<EntityApp>(chart, showing, command, l.start));
}



bool EntityCommand::AddAttribute(const Attribute &)
{
    return false;
}

void EntityCommand::AttributeNames(Csh &)
{
    return;
}

bool EntityCommand::AttributeValues(std::string_view, Csh &)
{
    return false;
}

void EntityCommand::AttachComment(Note *cn)
{
    _ASSERT(CanBeNoted());
    _ASSERT(entities.size());
    (*entities.rbegin())->AttachComment(cn);
}

/** Take ownership of the note and store it temporarily.
 * This is called for all notes pointing to entity commands before
 * PostParseProcess. It allows us to combine entity commands and
 * figure out where the notes shall point to.
 * We also store what is the name of the entity the note is made to.
 * You can store notes before EntityCommand::PostParseProcess.
 * These will be removed in MscChart::PostParseProcessArcList, and will be
 * inserted into the arc list after the commandEntity.*/
bool EntityCommand::StoreNoteTemporarily(Note *cn)
{
    if (internally_defined) return false;
    //target_entity may be totally empty here, if we were created as a
    //result of a 'heading' or 'show' command.
    //In this case, if we get merged with another entity command,
    //it will be lost, where the note was made and it will default to
    //the last of the combined series. (Not good, but we can live with it.)
    tmp_stored_notes.emplace_back(std::unique_ptr<Note>(cn), target_entity);
    return true;
}

/** Take the temporarily stored notes and insert them into the list of arcs.
 * Since the target of temporarily stored notes are stored as "name of entity"
 * we also resolve these by looking up the EntityApp for the entity name
 * and set the target of the note to that EntityApp object.
 * If the target entity of a note has disappeared due to a collapsed parent,
 * we silently drop the note, as well (will not show up in chart).
 * @param list The arc list to insert the note into.
 * @param after The position after which to insert.*/
void EntityCommand::ReinsertTmpStoredNotes(ArcList &list, ArcList::iterator after)
{
    _ASSERT(entities.size()); //After PostParseProcess, we must have entities even if a heading command
    after++;  // list::insert insterts _before_ the given iterator

    //If we started out as a 'heading' or 'show' command, we may have no
    //target_entity set at creation. Now we simply take the last entity.
    if (target_entity.length()==0)
        target_entity = entities.back()->name;

    while (tmp_stored_notes.size()) {
        //calculate the target of the note.
        const string *target_name;
        //if the note has its own 'at' clause and it is an entity, use that
        if (tmp_stored_notes.front().note->GetPointTowardText().length() &&
            chart->AllEntities.Find_by_Name(tmp_stored_notes.front().note->GetPointTowardText())!=nullptr)
            target_name = &tmp_stored_notes.front().note->GetPointTowardText();
        //if not, then let us use the target stored with it.
        else if (tmp_stored_notes.front().target.length())
            target_name = &tmp_stored_notes.front().target;
        //if no such target, we can simply use the last entity of the entity command.
        else
            target_name = &target_entity;

        //here "target_name" can be empty, if entities are virtual
        if (target_name->length()) {
			EntityRef ent = chart->AllEntities.Find_by_Name(*target_name);
			_ASSERT(ent);
			EntityRef ent_parent = chart->FindActiveParentEntity(ent);
            if (ent_parent == ent) {
                for (auto &e : entities)
                    if (e->entity == ent) {
                        //set target of note
                        tmp_stored_notes.front().note->SetTarget(e.get());
                        list.insert(after, std::move(tmp_stored_notes.front().note));  //we move the object out from front(), which itself will be removed below
                        break;
                    }
            }
        }
        //OK, we either reinserted or deleted the note, remove from tmp list
        tmp_stored_notes.erase(tmp_stored_notes.begin());
    }
}

/** Move EntityApp objects and temporarily stored notes to 'e`.
 * Effectively empties us.*/
void EntityCommand::MoveMyContentAfter(EntityAppHelper &e)
{
    e.entities.splice(e.entities.end(), entities);
    std::ranges::move(tmp_stored_notes, std::back_inserter(e.notes));
    tmp_stored_notes.clear();
}

/** Append a list of EntityApp objects to our list of EntityApps.
 * (This is called only from EntityCommand::Combine() to merge two
 * subsequent EntityCommand objects and from MscChart::PostParseProcess()
 * for auto generated entities. In any case definitely after
 * AddAttributeList() and before PostParseProcess().)
 * We take care not to have one entity mentioned twice.
 * Thus if an entity referenced by an EntityApp being appended is
 * already mentioned by an EntityApp already on our list, we
 * combine their attributes. Below are the members of EntityApp
 * with comments on  * how to merge a later EntityApp into a former one.
 * - name;                //This is const, shall be the same
 * - label;               //This can only be set once: keep the former
 * - linenum_label_value; //Location of label text in input file, should only matter in first EntityApp
 * - pos;                 //This was used only in AddAttributeList - ignore
 * - rel;                 //This was used only in AddAttributeList - ignore
 * - collapsed;           //This was used only in AddAttributeList - ignore
 * - show;                //The latter (if set) shall overwrite the former one
 * - active;              //The latter (if set) shall overwrite the former one
 * - show_is_explicit;    //Ignore, this was only used in ApplyPrefix() which is only called during parse
 * - entity;                 //This will be set during PostParseProcess(), ignore
 * - style;               //This will be finalized during PostParseProcess(), combine latter into former
 * - parsed_label;        //Will be set during PostParseProcess(), ignore here
 * - defining;            //keep former
 * - shown;               //ignore, will be set in PostParseProcess()
 * Note that 'e' will be moved from at the end.*/
void EntityCommand::AppendToEntities(EntityAppList &&e)
{
    for (auto &i : e)
        if (i->name.length()==0) {
            //always copy a multi-EntityApp.
            entities.Append(std::move(i));
        } else {
            auto i2 = std::find_if(entities.begin(), entities.end(), [&i](auto &ent) {return ent->name==i->name; });
            if (i2 == entities.end()) {
                entities.Append(std::move(i));
            } else {
                (*i2)->style += i->style;
                if (i->show)
                    (*i2)->show = i->show;
                if (i->active)
                    (*i2)->active = i->active;
            }
    }
}

/** Combine (merge the content of) two subsequent EntityCommand objects
 * We merge then only if both or neither contain internally defined
 * EntityApps (resulting from comment.* chart options).
 * We take care to have one entity referenced only by a single EntityApp
 * object even after the merge.
 * `ce` will be effectively emptied out after.*/
void EntityCommand::Combine(EntityCommand *ce)
{
    if (!ce) return;
    if (!ce->valid) return;
    _ASSERT(ce->internally_defined == internally_defined);
    //If both file_pos are valid, keep ours.
    if (file_pos.IsInvalid())
        file_pos = ce->file_pos;
    AppendToEntities(std::move(ce->entities));
    ce->entities.clear();
    //move potential notes, as well
    std::ranges::move(ce->tmp_stored_notes, std::back_inserter(tmp_stored_notes));
    ce->tmp_stored_notes.clear();
    target_entity = ce->target_entity;
    CombineComments(ce); //moves notes from 'ce' to us
}

/** Apply the relevant attributes if the entity command was prefixed with
 * "show", "hide", "activate" or "deactivate" or "centerline".
 * Take care that if the entity had a specific show or active attribute, such
 * attributes take precedence over the prefix.*/
EntityCommand *EntityCommand::ApplyPrefix(std::string_view prefix)
{
    _ASSERT(!internally_defined);
    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");
    for (auto &pEntityApp : entities)
		if (show_or_hide) {
			if (pEntityApp->show_is_explicit) continue;
			pEntityApp->show = show;
        } else if (activate_or_deactivate) {
            if (pEntityApp->active_is_explicit) continue;
            pEntityApp->active = {activate, pEntityApp->file_pos.start};
        }
    //try to align activations and deactivations to the centerline of previous object
    if (activate_or_deactivate || CaseInsensitiveEqual(prefix, "centerline"))
        for (auto &pEntityApp : entities)
            pEntityApp->centerlined = true;
    return this;
}

/** Finds (or adds) the EntityApp object for an entity in our list.
 * If our list already contains an EntityApp referencing `entity`, we return it.
 * If not, we append an EntityApp referencing `entity` using that
 * entity's running style and `l` as file position. */
//Adds and entitydef for "entity" uses running_show and running_style
EntityApp* EntityCommand::FindAddEntityDefForEntity(const string &entity,
                                                    const FileLineColRange &l)
{
    //find if already have a def for this
    for (auto &eapp : entities)
        if (eapp->name == entity) return eapp.get();
    const EntityRef jj_ent = chart->AllEntities.Find_by_Name(entity);
    _ASSERT(jj_ent);
    EntityApp *ed = new EntityApp(entity.c_str(), chart);
    ed->entity = jj_ent;
    ed->style = jj_ent->running_style;
    ed->file_pos = l;
    ed->show.reset();
    ed->active.reset();
    entities.Append(ed);
    return ed;
}


/** Determine status based on the running status of ultimate children.
 * If this is not a grouped entity (has no child entities) we return
 * its running status (both activation and show).
 * If it has children, we OR their activation and show status
 * (return active if any of them are active and showing if any of them are).
 * This goes recursive, thus if any of the children has children, we ignore
 * its running status and combine the running status of the children.
 * Thus ultimately only the running status of leaf entities will be used.*/
EEntityStatus EntityCommand::GetCombinedStatus(const Entity& entity) const
{
    if (entity.children_names.empty()) return entity.running_shown;
    EEntityStatus ret = EEntityStatus::SHOW_OFF;
    for (const auto &s : entity.children_names) {
        auto i = chart->AllEntities.Find_by_Name(s);
        _ASSERT(i);
        const EEntityStatus es = GetCombinedStatus(*i);
        if (es.IsActive()) ret.Activate(true);
        if (es.IsOn()) ret.Show(true);
    }
    return ret;
}

/* Process the list of EntityApps after parsing.
 * The following rules apply.
 * Each entity we can possibly have is by now represented in chart->AllEntities and
 * is of class Entity.
 * Each time we name an entity in an entity command, we allocate an EntityApp object.
 * If the "defining" member is true this mention of the entity was used to define the
 * entity. If it is false, this mention of the entity is a subsequent one.
 * An entity can have zero or one EntityApps with defining=true (zero if the entity
 * was implicitly defined via e.g., an arrow definition, in this case it is called
 * an automatically generated entity and an EntityApp is placed into MscChart::AutoGenEntities).
 * When an entity is mentioned in an entity command any (or all) of the three things
 * can be done: turn it on/off, activate/deactivate it or change the style. An entity
 * that is on can be turned on again, which forces the drawing of an entity heading of it.
 * Similar, the heading command draws an entity heading for all entities that are currently
 * on. This is emulated by adding EntityApps for such entities with show set to on.
 *
 * During the PostParse process we keep up-to date the "running_show" and "running_style"
 * members of the entities. (Members of class "Entity".) These are used by arrows, etc.
 * to find out if certain entities are on/off or active, etc.
 * In addition, we copy the actual "running_style" and "running_show" in every EntityApp,
 * as well; and then in the PosPos process, when x and y coordinates are already set,
 * we add them to Entity::status map, so that when we draw entity lines we know what to
 * draw at what coordinate. The entity headings are drawn based on the EntityApp::style
 * member.
 *
 * The EntityApp::draw_heading member tells if a heading should be drawn for this heading
 * at this point. The "show" and "active" members tell if there was "show" or "active"
 * attributes set (perhaps via show/hide/activate/deactivate commands). If so, the
 * "show_is_explicit"/"active_is_explicit" are set. (This is used to prevent overwriting
 * an explicitly written [show=yes/no] with a "show" command. Thus 'show aaa [show=no]'
 * will result in show=false. Otherwise any newly created EntityApp objects
 * has show set to true and active set to false - the default for newly defined entities.

 * The algorithm here is the following.
 *
 * 0. Expand any multi-EntityApps to several entities. If an entityApp has an empty
 *    name, it refers to either all showing or non-showing entities (depending on its
 *    'only_shown_if_multi' member. These EntityApps can only cause state change,
 *    that is we only take a look at their 'show' and 'active' members.
 *    Such EntityApps are created by commands such as 'heading', 'show', 'deactivate'
 *    and also for automatic headings at pagination.
 * 1. Merge EntityApps corresponding to the same entity. (They are in the same order
 *    as in the source file, so copying merging "active", "show" and "style" is enough,
 *    since all other attributes can only be used at definition, which can be the first
 *    one only.)
 * 2. Apply the style elements specified by the user to the entity's "running_style"
 * 3. If a grouped entity is listed with show=yes/no, we copy this attribute to all of its
 *    listed children (unless they also have an explicit show attribute, which overrides that
 *    of the group). We also prepend an EntityApp for any unlisted children and copy
 *    the attribute.
 *    At the same time, we remove the show attribute from the grouped entities.
 *    This will result in that if a grouped entity is turned on or off all of its
 *    children (and their children, too) will get turned on or off, as well.
 * 4. Apply the show and active attributes of all listed entities to
 *    "Entity::running_show" & "EntityApp::draw_heading".
 * 5. Update the "running_shown" of all parents based on the status of leaf children,
 *    if any change, add a corresponding EntityApp (if needed)
 * 6. Order the listed entities such that parents come first and their children after
 * 7. Set "draw_heading" of the children of collapsed parents to false
 * 8. For those that draw a heading, update "left" and "right", process label & record max width
 */
ArcBase* EntityCommand::PostParseProcess(Canvas &canvas, bool hide, EntityRef &left, EntityRef &right,
                                         Numbering&, MscElement** target, ArcBase* /*byref_target*/)
{
    if (!valid) return nullptr;

    //0. The very first thing to do is to expand multi-EntityApps
    // During this pass we also update 'Entity::running_defined'
    for (auto i_app = entities.begin(); i_app != entities.end(); /*nope*/) {
        if ((*i_app)->name.length()) {
            const EntityRef j_ent = chart->AllEntities.Find_by_Name((*i_app)->name);
            (*i_app)->entity = j_ent;
            if ((*i_app)->defining && (*i_app)->entity)
                (*i_app)->entity->running_defined = true;
            i_app++;
            continue;
        }
        for (auto &e : chart->ActiveEntities) {
            //skip entities defined only later
            if (!e->running_defined) continue;
            //if the entity appears before i_app and is turned on,
            //we include it. If it does not appear, we include only if
            //it shows.
            bool showing = e->running_shown.IsOn();
            for (auto i2 = i_app; i2!=entities.begin(); i2--) {
                const EntityApp &ee = **std::prev(i2);
                if (ee.name==e->name && ee.show) {
                    showing = *ee.show;
                    break;
                }
            }
            if ((*i_app)->only_shown_if_multi != showing) continue;
            if (e->children_names.size()) continue;
            if (chart->IsVirtualEntity(e)) continue;
            //insert before i_app
            std::unique_ptr<EntityApp> ed = std::make_unique<EntityApp>(e->name.c_str(), chart);
            ed->entity = e;
            ed->style = e->running_style;
            ed->file_pos.start = ed->file_pos.end = (*i_app)->active.file_pos;
            ed->show = (*i_app)->show;
            ed->active = (*i_app)->active;
            ed->show_is_explicit = ed->active_is_explicit = true;
            entities.insert(i_app, std::move(ed));
        }
        entities.erase(i_app++);
    }

    //Ok, we may have no entities here.
    if (entities.size()==0)
        return nullptr;

    //1. First merge entitydefs of the same entity, so that we have at most one for each entity.
    for (auto i_app = entities.begin(); i_app != entities.end(); /*nope*/) {
        //find first entity of this name
        EntityApp *ed = FindAddEntityDefForEntity((*i_app)->name, file_pos); //second param dummy, we never add here
        if (ed == i_app->get()) i_app++;
        else {
            //OK, "ed" is an EntityApp before i_app, combine them.
            _ASSERT(!(*i_app)->defining);
            //show_is_explicit and active_is_explicit makes no role beyond this, so ignore
            ed->Combine(i_app->get());
            //Ok, delete i_app
            entities.erase(i_app++);
        }
    }

    //2. Then apply the style changes of this command to the running style of the entities
    for (auto &i_app : entities) {
        //Make the style of the entitydef fully specified using the accumulated style info in Entity
        i_app->entity->running_style += i_app->style;  //i_app->style is a partial style here specified by the user
    }
    //3. Copy show on/off attribute from grouped entities to their children
    for (auto &i_app : entities) {
        //if the entity is a grouped entity with a show/hide attribute,
        //add an entitydef to our list for those children, who have no entitidefs
        //yet in the list. For those children, who have, just set show attribute
        //if not set yet
        //new entitydefs are added to the end of the list - and get processed for further
        //children
        if (i_app->show && i_app->entity->children_names.size())
            for (auto ss : i_app->entity->children_names)
                FindAddEntityDefForEntity(ss, i_app->file_pos)->show = i_app->show;
    }
    //4. Decide if we will draw a heading for these entities & update running state
    for (auto &i_app : entities) {
        //First, set the show and active attributes if we define the entity here and the
        //user has not set any of them.
        if (i_app->defining) {
            if (!i_app->show)
                i_app->show = true;  //new entities are shown by default
            if (!i_app->active)
                i_app->active = false; //.. but are not active
        }
        //Decide, if this entitydef will draw a heading or not
        //It can get drawn because we 1) said show=yes, or
        //2) because it is on, we mention it (without show=yes) and it is
        //a full heading.
        //But it is never shown for virtual entities
        i_app->draw_heading = i_app->show == true;  //set and to true
        if (chart->IsVirtualEntity(i_app->entity))
            i_app->draw_heading = false;
        //Adjust the running status of the entity, this is valid *after* this command.
        //This is just for the Height process knows which entity is on/off
        if (i_app->show)
            i_app->entity->running_shown.Show(*i_app->show);
        //Update the style of the entitydef
        i_app->style = i_app->entity->running_style;	 //i_app->style now become the full style to use from this point
        //reflect any "active" attribute in the running_shown variable
        if (i_app->active)
            i_app->entity->running_shown.Activate(*i_app->active);
    }
    //5. Now we are guaranteed to have all leaf children's running_shown status right.
    //However, we can have parents, whose child(ren) changed status and we need to update that
    //We will need to add EntityDefs here for such parents (so they update status in PostPos, too)
    for (auto &e : chart->AllEntities) {
        if (e.children_names.size()==0) continue;
        EEntityStatus es_new = GetCombinedStatus(e);
        EEntityStatus es_old = e.running_shown;
        if (es_old == es_new) continue;
        //ok, shown status has changed, add/lookup entitydef
        EntityApp *ed = FindAddEntityDefForEntity(e.name, this->file_pos);
        if (es_new.IsOn() != es_old.IsOn()) {
            ed->show = es_new.IsOn();
            ed->large_appears = ed->show && ed->entity->large && !ed->entity->collapsed;
        }
        if (es_new.IsActive() != es_old.IsActive())
            ed->active = es_new.IsActive();
        //If all EntityApps that are descendants of the newly added parent
        //are centerlined, we also centerline the added entityApp. This helps
        //if the added parent EntityApp is collapsed and arrows to children
        //entities will be drawn to the parent.
        std::optional<bool> centerlined;
        for (auto& i_app : entities)
            if (centerlined==false) break;
            else if (chart->IsMyParentEntity(i_app->entity, ed->entity)) centerlined = i_app->centerlined;
        if (centerlined==true) ed->centerlined = true;
        e.running_shown = es_new;
    }


    //6. Order the list (lousy bubblesort)
    //any descendant should come after any of their anscestors, but we can only compare
    //direct parent-child, so we go through the list until we see change
    bool changed;
    do {
        changed = false;
        //Warning! We do not proces the first entity below
        for (auto i_app = ++entities.begin(); i_app != entities.end(); i_app++)
            for (auto i_app2 = entities.begin(); i_app2 != i_app; i_app2++)
                if ((*i_app2)->entity->parent_name == (*i_app)->name) {
                    std::swap(*i_app, *i_app2);
                    changed = true;
                }
    } while (changed);

    //At this point the list contains all entities that
    //- is mentioned by the user (style or status change)
    //- shall be shown a heading due to heading command
    //- has a parent (ancestor) or children (descendant) in the above mentioned two categories
    //Collapse is not considered yet, we will do it below

    //7. Finally prune the list: do not show those that shall not be displayed due to collapse
    // Also, if any header is shown, kill "centerline"
    for (auto &i_app : entities)  {
        if (chart->FindActiveParentEntity(i_app->entity) != i_app->entity)
            i_app->draw_heading = false;
        if (i_app->draw_heading)
            i_app->centerlined = false;
    }

    //8. If we remained centerline, go back to our target (if an DirArrow)
    //so that it can update activate status of its entities
    //In addition, inform the EntityApp object about where they should
    //make effect (the centerline of '*target')
    //*target may be DELETE_NOTE, which means that the target before us have been
    //deleted. We treat it as if there were no target.
    //Need to watch for this as dynamic casting DELETE_NOTE crashes.
    DirArrow * const prev = dynamic_cast<DirArrow *>(*target==DELETE_NOTE ? nullptr : *target);
    bool had_one = false;
    for (auto &pEntityApp : entities) {
        pEntityApp->centerlined &= (prev!=nullptr);
        if (pEntityApp->centerlined) {
            pEntityApp->centerline_target = prev;
            had_one = true;
        }
    }
    if (had_one)
        prev->UpdateActiveSizes();

    //9. At last we have all entities among "entities" that will show here/change status or style
    //Go through them and update left, right and the entities' maxwidth
    //But if we are hidden, then skip updating maxwidth and kill drawing the header
    //Also, set target to the entity we received in the constructor
    for (auto &i_app : entities) {
        if (target_entity == i_app->name && !internally_defined)
            *target = i_app.get();
        if (!i_app->draw_heading) continue;
        left =  chart->EntityMinByPos(left,  chart->FindWhoIsShowingInsteadOf(i_app->entity, true));
        right = chart->EntityMaxByPos(right, chart->FindWhoIsShowingInsteadOf(i_app->entity, false));
        i_app->parsed_label.Set(i_app->entity->label, canvas, chart->Shapes, i_app->entity->running_style.read().text);
        if (hide)
            i_app->draw_heading = false;
        else
            i_app->entity->maxwidth = std::max(i_app->entity->maxwidth, i_app->Width());
    }
    if (target_entity.length()==0 && entities.size())
        *target = entities.back().get();
    return this;
}


//Here we have EIterators in entitydefs that point to AllEntities (in contrast to
//all other arcs, where PostParseProcess will convert to iterators in AllActiveEntities)
//If an entity we list here is not collapsed and have children, then it will
//be drawn as containing other entities.
//Since parents are in the beginning of the list, we will go and add distances from the back
//and use the added distances later in the cycle when processing parents
void EntityCommand::Width(Canvas &, DistanceRequirements &vdist)
{
    if (!valid) return;
    //Add a new element to vdist
    vdist.AddElementTop(this);
    //Add activation status right away
    AddEntityLineWidths(vdist);
    //Add distances for entity heading
    //Start by creating a map in which distances are ordered by index
    std::map<int, pair<double, double>> dist; //map the index of an active entity to spaces left & right
    //in PostParseProcess we created an entitydef for all entities shown here.
    //"full_heading" not even checked here
    for (auto i = entities.rbegin(); !(i==entities.rend()); i++) {
        //Take entity height into account or draw it if show=on was added
        //..or if we are a non-collapsed large group entity
        if (!(*i)->draw_heading && (!(*i)->entity->large || (*i)->entity->collapsed)) continue;
        if ((*i)->entity->children_names.size() == 0 || (*i)->entity->collapsed) {
            const double halfsize = (*i)->entity->maxwidth/2;
            const unsigned index = (*i)->entity->index;
            dist[index] = pair<double, double>(halfsize, halfsize);
            (*i)->right_ent = (*i)->left_ent = (*i)->entity;
            (*i)->right_offset = (*i)->left_offset = 0;
            vdist.InsertEntity((*i)->entity);
        } else {
            //grouped entity, which is not collapsed
            //find leftmost and rightmost active entity
            //and expand us by linewidth and space
            const EntityRef j_ent = (*i)->entity;
            double expand = chart->boxVGapInside + (*i)->style.read().line.LineWidth();
            (*i)->left_ent = chart->FindWhoIsShowingInsteadOf(j_ent, true);
            (*i)->right_ent= chart->FindWhoIsShowingInsteadOf(j_ent, false);
            (*i)->left_offset = dist[(*i)->left_ent->index].first += expand;
            (*i)->right_offset = dist[(*i)->right_ent->index].second += expand;
            //If this is a group entity containing only one element, ensure its label fit
            if ((*i)->left_ent == (*i)->right_ent) {
                if (dist[(*i)->left_ent->index].first < (*i)->entity->maxwidth/2) {
                    dist[(*i)->left_ent->index].first = (*i)->entity->maxwidth/2;
                    (*i)->left_offset = (*i)->entity->maxwidth/2;
                }
                if (dist[(*i)->right_ent->index].second < (*i)->entity->maxwidth/2) {
                    dist[(*i)->right_ent->index].second = (*i)->entity->maxwidth/2;
                    (*i)->right_offset = (*i)->entity->maxwidth/2;
                }
            } else {
                //Insert a requirement between left_ent and right_ent, so that our width will fit (e.g., long text)
                vdist.Insert((*i)->left_ent->index, (*i)->right_ent->index,
                             (*i)->entity->maxwidth - (*i)->left_offset - (*i)->right_offset);
            }
            vdist.InsertEntity((*i)->left_ent);
            vdist.InsertEntity((*i)->right_ent);
        }
    }
    if (dist.size()) {
        //Now convert neighbouring ones to box_side distances, and add the rest as normal side distance
        vdist.Insert(dist.begin()->first, DISTANCE_LEFT, dist.begin()->second.first); //leftmost distance
        vdist.Insert(dist.rbegin()->first, DISTANCE_RIGHT, dist.rbegin()->second.second); //rightmost distance
        for (auto d = dist.begin(); d!=--dist.end(); d++) {
            auto d_next = std::next(d);
            vdist.Insert(d->first, DISTANCE_RIGHT, d->second.second);
            vdist.Insert(d_next->first, DISTANCE_LEFT, d_next->second.first);
        }
    }
    //Now add some distances for activation (only for non-grouped or collapsed entities)
    for (auto &pEntityApp : entities) {
        if (pEntityApp->entity->children_names.size() == 0 || pEntityApp->entity->collapsed) {
            if (pEntityApp->show)
                pEntityApp->entity->running_shown.Show(*pEntityApp->show);
            if (pEntityApp->active)
                pEntityApp->entity->running_shown.Activate(*pEntityApp->active);
        }
    }
    //Add a new element to vdist
    vdist.AddElementBottom(this);
    //Add activation status right away
    AddEntityLineWidths(vdist);
}

//Here we add to "cover", do not overwrite it
void EntityCommand::LayoutCommentsHelper(Canvas &canvas, AreaList *cover, double &l, double &r)
{
    for (auto i_app = entities.begin(); i_app!=entities.end(); i_app++)
        (*i_app)->LayoutCommentsHelper(canvas, cover, l, r);
    Command::LayoutCommentsHelper(canvas, cover, l, r); //sets comment_height
}

void EntityCommand::Layout(Canvas &canvas, AreaList *cover)
{
    if (!valid) return;
    Range hei(0,0);
    //Those entities explicitly listed will have their own EntityApp for this line.
    //Thus their area will be stored there and not in EntityCommand->area
    //But, still put those into "cover" so they can be considered for placement
    //There are other entities shown here, those triggered by a heading command.
    //They have the line info of the entity command.

    //We go backwards, so that contained entities get calculated first
    unsigned num_showing = 0;
    for (auto i = entities.rbegin(); !(i==entities.rend()); i++) {
        if (!(*i)->draw_heading && (!(*i)->entity->large || (*i)->entity->collapsed)) {
            (*i)->AddAreaImportantWhenNotShowing();
            continue;
        }
        //Collect who is my children in this list
        NPtrList<EntityApp> edl;
        for (auto ii = entities.rbegin(); !(ii==entities.rend()); ii++)
            if ((*ii)->draw_heading && ii->get() != i->get() && chart->IsMyParentEntity((*ii)->name, (*i)->name))
                edl.Append(ii->get());
        //EntityApp::Height places children entities to yPos==0
        //Grouped entities may start at negative yPos.
        //We collect here the maximum extent
        //Note: Height() also adds the cover to the entitydef's area and
        //fills (*i)->area_important
        Area entity_cover;
        hei += (*i)->Height(entity_cover, edl);
        if (cover)
            *cover += GetCover4Compress(entity_cover);
        //area += (*i)->GetAreaToSearch();
        //area_to_note += (*i)->GetAreaToNote();
        if ((*i)->draw_heading)
            num_showing++;
    }
    if (num_showing) {
        _ASSERT(!internally_defined); //internally defined entitydefs should not show a heading
        //Ensure overall startpos is zero
        ShiftBy(-hei.from + chart->headingVGapAbove);
        yPos = 0; //ruined by ShiftBy() above, but must be always 0 in Layout()
        if (cover)
            cover->Shift(XY(0,-hei.from + chart->headingVGapAbove));
        height = chart->headingVGapAbove + hei.Spans() + chart->headingVGapBelow;
    } else
        height = 0; //if no headings show
    LayoutComments(canvas, cover);
}

Range EntityCommand::GetVisualYExtent(bool include_comments) const
{
    Range ret(yPos, yPos);
    if (valid)
        for (auto &pEntityApp : entities)
            if (pEntityApp->draw_heading) {
                ret += pEntityApp->outer_edge.y;
                ret += pEntityApp->outer_edge.y.till + *pEntityApp->style.read().shadow.offset;
            }
    if (include_comments && valid)
        ret += yPos+comment_height;
    return ret;

}

void EntityCommand::ShiftBy(double y)
{
    if (!valid) return;
    for (auto &pEntityApp : entities)
        pEntityApp->ShiftBy(y);
    Command::ShiftBy(y);
}

//We never split a heading. Here we should add a full_header if addHeader is set: TODO.
//Rght now we just update the running state in MscChart::AllEntities
double EntityCommand::SplitByPageBreak(Canvas &/*canvas*/, double /*netPrevPageSize*/,
                                    double /*pageBreak*/, bool &/*addCommandNewpage*/,
                                    bool addHeading, ArcList &/*res*/)
{
    if (addHeading)
        for (auto &pEntityApp : entities) {
            pEntityApp->entity->running_draw_pass = pEntityApp->draw_pass;
            //We ignore active state, just store on/off
            if (pEntityApp->show)
                pEntityApp->entity->running_shown = *pEntityApp->show ? EEntityStatus::SHOW_ON : EEntityStatus::SHOW_OFF;
            pEntityApp->entity->running_style = pEntityApp->style;
        }
    return -1; //we could not split
}


void EntityCommand::PostPosProcess(Canvas &canvas, Chart *ch)
{
    if (!valid) return;
    Command::PostPosProcess(canvas, ch);
    for (auto &pEntity : entities)
        pEntity->PostPosProcess(canvas, ch);
    if (height>0) {
        if (chart->headingSize == 0) chart->headingSize = yPos + height;
        chart->headingSize = std::min(chart->headingSize, yPos + height);
    }
}

void EntityCommand::RegisterLabels()
{
    if (valid)
        for (auto &pEntity : entities)
            pEntity->RegisterLabels();
}

void EntityCommand::CollectIsMapElements(Canvas &canvas)
{
    if (valid)
        for (auto &pEntity : entities)
            pEntity->CollectIsMapElements(canvas);
}

void EntityCommand::RegisterCover(EMscDrawPass pass) const
{
    if (!valid) return;
    for (auto &pEntity : entities)
        if (pEntity->draw_heading)
            pEntity->RegisterCover(pass);
}

void EntityCommand::Draw(Canvas &canvas, EMscDrawPass pass) const
{
    if (!valid) return;
    for (auto &pEntity : entities)
        if (const bool large = pEntity->entity->large && !pEntity->entity->collapsed
                ;  (pEntity->draw_heading || pEntity->large_appears)
                && ((pass==EMscDrawPass::BEFORE_ENTITY_LINES && large)
                   || (pass==draw_pass && !large)))
            pEntity->Draw(canvas);
}

//////////////////////////////////////////////////////////////////////////////////////

Newpage::Newpage(MscChart *msc, bool m) :
    Command(EArcCategory::NEWPAGE, msc),
    auto_heading_attr(msc->MyCurrentContext().auto_heading.value_or(false)), //If the page break is in a procedure this may be unset - but then this object will be dropped anyway.
    manual(m)
{
    vspacing = 0;
}

bool Newpage::AddAttribute(const Attribute &a)
{
    if (a.Is("auto_heading")) {
        if (!a.EnsureNotClear(chart->Error, EStyleType::ELEMENT)) return true;
        if (!a.CheckType(EAttrType::BOOL, chart->Error)) return true;
        auto_heading_attr = a.yes;
        return true;
    }
    return false;
}

void Newpage::AttributeNames(Csh &csh)
{
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "auto_heading",
        "Turn this on to automatically generate an entity heading at the top of the new page.",
        EHintType::ATTR_NAME));
}

bool Newpage::AttributeValues(std::string_view attr, Csh &csh)
{
    if (CaseInsensitiveEqual(attr,"auto_heading")) {
        csh.AddYesNoToHints();
        return true;
    }
    return false;
}

ArcBase* Newpage::PostParseProcess(Canvas &canvas, bool hide, EntityRef &left,
                                          EntityRef &right, Numbering &number,
                                   MscElement** note_target, ArcBase* byref_target)
{
    if (auto_heading_attr && !autoHeading) {
        autoHeading = std::make_unique<EntityCommand>(chart, "heading", FileLineColRange());
        autoHeading->AddAttributeList(nullptr);
        EntityRef dummy1 = nullptr;
        EntityRef dummy2 = nullptr;
        Numbering dummy3; //will not be used by a EntityCommand
        MscElement *dummy4 = nullptr; //target of any notes, pretend we have none
        autoHeading->PostParseProcess(canvas, false, dummy1, dummy2, dummy3, &dummy4, nullptr);
        chart->Progress.DoneItem(EArcSection::POST_PARSE, autoHeading->myProgressCategory);
    }
    return Command::PostParseProcess(canvas, hide, left, right, number, note_target, byref_target);
}

void Newpage::FinalizeLabels(Canvas &)
{
    if (autoHeading)
        chart->Progress.DoneItem(EArcSection::FINALIZE_LABELS, autoHeading->myProgressCategory);
}

void Newpage::Width(Canvas &canvas, DistanceRequirements &/*vdist*/)
{
    if (autoHeading) {
        //Do not add space requirements for verticals here - they will never
        //conflict with auto headings.
        DistanceRequirements vd;
        autoHeading->Width(canvas, vd);
        chart->Progress.DoneItem(EArcSection::WIDTH, autoHeading->myProgressCategory);
    }
}


void Newpage::Layout(Canvas &canvas, AreaList *cover)
{
    Command::Layout(canvas, cover);
    if (autoHeading) {
        autoHeading->Layout(canvas, nullptr);
        chart->Progress.DoneItem(EArcSection::LAYOUT, autoHeading->myProgressCategory);
    }
}

void Newpage::ShiftBy(double y)
{
    Command::ShiftBy(y);
    //Shift autoheading to be just above us, irrespective of where it was before
    if (autoHeading)
        autoHeading->ShiftBy(yPos - autoHeading->GetFormalYExtent().till);
}


void Newpage::CollectPageBreak()
{
    if (!valid) return;
    chart->AddPageBreak(yPos, manual, autoHeading.get());
}

void Newpage::PlaceWithMarkers(Canvas &/*cover*/)
{
    if (autoHeading)
        chart->Progress.DoneItem(EArcSection::PLACEWITHMARKERS, autoHeading->myProgressCategory);
}

void Newpage::PostPosProcess(Canvas &/*cover*/, Chart * /*ch*/)
{
    if (autoHeading)
        chart->Progress.DoneItem(EArcSection::POST_POS, autoHeading->myProgressCategory);
}

void Newpage::Draw(Canvas &/*canvas*/, EMscDrawPass /*pass*/) const
{
    //We cheat here. These will not be drawn only if a single page
    //is being drawn - but there we will not report them ready.
    if (autoHeading)
        chart->Progress.DoneItem(EArcSection::DRAW, autoHeading->myProgressCategory);
}



//////////////////////////////////////////////////////////////////////////////////////

void SetBackground::PostPosProcess(Canvas &/*canvas*/, Chart * /*ch*/)
{
    if (!valid) return;
    auto i = chart->Background.find(yPos);
    if (i == chart->Background.end())
        chart->Background[yPos] = fill;
    else
        i->second += fill;
}

//////////////////////////////////////////////////////////////////////////////////////
ArcBase* SetNumbering::PostParseProcess(Canvas &/*canvas*/, bool /*hide*/, EntityRef &/*left*/, EntityRef &/*right*/,
                                            Numbering &number, MscElement ** /*target*/, ArcBase * /*byref_target*/)
{
    if (!valid) return nullptr;
    if (set_length)
        number.SetSize(unsigned(set_length));
    if (inc_by)
        number.increment(inc_by);
    return this;
}

//////////////////////////////////////////////////////////////////////////////////////

Marker::Marker(std::string_view m, FileLineColRange ml, MscChart *msc) :
    Command(EArcCategory::TINY_EFFORT, msc), name(m),
    target(nullptr), offset(0)
{
    if (chart->SkipContent()) {
        //When parsing a procedure or design lib, do not register the marker
        valid = false;
        return;
    }
    auto marker = chart->GetMarker(name);
    if (marker == nullptr) {
        chart->Markers[name].line = ml.start;
        chart->Markers[name].y = -1001;
        offset = 0;
        return;
    }
    if (marker->line.IsInvalid()) {
        chart->Error.Error(ml.start, "Marker '"+StringFormat::ConvertToPlainTextCopy(name)+"' is a built-in marker. Ignoring this command.", "Try using another name.");
    } else {
        chart->Error.Error(ml.start, "Marker '"+StringFormat::ConvertToPlainTextCopy(name)+"' has already been defined. Keeping old definition.");
        chart->Error.Error(marker->line, ml.start, "Location of previous definition.");
    }
    valid = false;
}

bool Marker::AddAttribute(const Attribute &a)
{
    if (a.Is("offset")) {
        if (!a.EnsureNotClear(chart->Error, EStyleType::ELEMENT)) return true;
        if (!a.CheckType(EAttrType::NUMBER, chart->Error)) return true;
        offset = a.number;
        return true;
    }
    if (a.Is("centerline")) {
        if (!a.EnsureNotClear(chart->Error, EStyleType::ELEMENT)) return true;
        if (!a.CheckType(EAttrType::STRING, chart->Error)) return true;
        if (CaseInsensitiveEqual(a.value, "yes"))
            centerline = CenterlineSelector::AT_DST;
        else if (CaseInsensitiveEqual(a.value, "source"))
            centerline = CenterlineSelector::AT_SRC;
        else if (CaseInsensitiveEqual(a.value, "destination"))
            centerline = CenterlineSelector::AT_DST;
        else if (CaseInsensitiveEqual(a.value, "left"))
            centerline = CenterlineSelector::LEFT_SIDE;
        else if (CaseInsensitiveEqual(a.value, "right"))
            centerline = CenterlineSelector::RIGHT_SIDE;
        else if (CaseInsensitiveEqual(a.value, "no"))
            centerline.reset();        
        return true;
    }
    return ArcBase::AddAttribute(a);
}

void Marker::AttributeNames(Csh &csh)
{
    ArcBase::AttributeNames(csh);
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME)+"offset",
        "Add this many pixels to the current location and mark that place.",
        EHintType::ATTR_NAME));
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME)+"centerline",
        "Specifying this attribute will set the marker at the centerline of the previous element.",
        EHintType::ATTR_NAME));
}

bool Marker::AttributeValues(std::string_view attr, Csh &csh)
{
    if (CaseInsensitiveEqual(attr,"offset")) {
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable()+"<number>",
            "Interpreted in pixels.",
            EHintType::ATTR_VALUE, false));
        return true;
    }
    if (CaseInsensitiveEqual(attr, "centerline")) {
        csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRVALUE)+"yes",
            "Set the marker at the centerline of the previous element.",
            EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForYesNo, 1));
        csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRVALUE)+"no",
            "Set the marker at the place of its definition.",
            EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForYesNo, 0));
        csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRVALUE)+"source",
            "Set the marker at the centerline of the previous element - at its source. Useful for slanted arrows.",
            EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForSide, CshHintGraphicParam(ESide::LEFT)));
        csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRVALUE)+"destination",
            "Set the marker at the centerline of the previous element - at its destination. Useful for slanted arrows.",
            EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForSide, CshHintGraphicParam(ESide::RIGHT)));
        csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRVALUE)+"left",
            "Set the marker at the centerline of the previous element - at its left side. Useful for slanted arrows.",
            EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForSide, CshHintGraphicParam(ESide::LEFT)));
        csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRVALUE)+"right",
            "Set the marker at the centerline of the previous element - at its right side. Useful for slanted arrows.",
            EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForSide, CshHintGraphicParam(ESide::RIGHT)));
        return true;
    }
    if (ArcBase::AttributeValues(attr, csh)) return true;
    return false;
}

ArcBase * Marker::PostParseProcess(Canvas & /*canvas*/, bool /*hide*/, EntityRef & /*left*/, EntityRef & /*right*/,
                                   Numbering& /*number*/, MscElement** /*note_target*/, ArcBase* byref_target)
{
    if (!valid) return nullptr;
    if (!centerline) return this;
    if (byref_target==nullptr) {
        chart->Error.Error(file_pos.start, "There is no element before to mark its centerline. Ignoring marker.");
        return nullptr;
    }
    //Allow marking an existing centerline marker
    Marker *m = dynamic_cast<Marker*>(byref_target);
    if (m && m->target)
        byref_target = m->target;
    if (!byref_target->HasCenterline()) {
        chart->Error.Error(file_pos.start, "The previous element has no centerline to mark (only boxes, pipes, arrows, entities and dividers have). Ignoring marker.");
        chart->Error.Error(byref_target->file_pos.start, file_pos.start, "This the element I attempted to take the centerline of.");
        return nullptr;
    }
    target = byref_target;
    return this;
}

void Marker::Width(Canvas &/*canvas*/, DistanceRequirements &vdist)
{
    if (centerline) {
        _ASSERT(target);
        vdist.AddMarkerTranslation(name, target);
    } else {
        //Add a new element to vdist
        vdist.AddMarker(name);
        //Add activation status right away
        AddEntityLineWidths(vdist);
    }
}

void Marker::ShiftBy(double y)
{
    //We only update the Markers[] array when we move.
    //I thought about it and it is sufficient in both the centerline and non-centerline cases
    if (!valid) return;
    Command::ShiftBy(y);
    if (centerline)
        chart->Markers[name].y = target->GetCenterline(*centerline)+offset;
    else
        chart->Markers[name].y = yPos+offset;
}

/////////////////////////////////////////////////////////////////
/** Call this with
 * 1. two entities
 * 2. one entity and a nullptr (in which case the nullptr will be replaced by RSide)
 * 3. nullptr and one entity (in which case the nullptr will be replaced by LSide)
 * 4. LNote and nullptr (will result in StartEntity-LNote)
 * 5. RNote and nullptr (will result in RNote-EndEntity)*/
AddHSpace::AddHSpace(MscChart*msc, const NamePair*enp) :
    Command(EArcCategory::TINY_EFFORT, msc),
    format(msc->MyCurrentContext().text)
{
    if (enp==nullptr) {
        valid=false;
        return;
    }
    if (enp->src.length()) {
        src = chart->FindAllocEntity(enp->src.c_str(), enp->sline);
        sline = enp->sline;
    } else
        src = chart->LSide;
    if (src == chart->LNote) {
        _ASSERT(!enp->dst.length());
        src = chart->StartEntity;
        dst = chart->LNote;
    } else if (src == chart->RNote) {
        _ASSERT(!enp->dst.length());
        dst = chart->EndEntity;
    } else if (enp->dst.length()) {
        dst = chart->FindAllocEntity(enp->dst.c_str(), enp->dline);
        dline = enp->dline;
    } else
        dst = chart->RSide;
    delete enp;
    if (src == dst && !chart->SkipContent()) {
        valid = false;
        msc->Error.Warning(sline.IsInvalid() ? dline.start : sline.start,
            "This command has no effect.",
            "You can only add horizontal space between two different entities.");
        return;
    }
}

bool AddHSpace::AddAttribute(const Attribute &a)
{
    if (a.Is("label")) {
        if (!a.CheckType(EAttrType::STRING, chart->Error)) return true;
        //EAttrType::CLEAR is OK above with value = ""
        label = a.value;
        return true;
    }
    if (a.Is("space")) {
        if (!a.CheckType(EAttrType::NUMBER, chart->Error)) return true;
        space = a.number;
        return true;
    }
    if (format.AddAttribute(a, chart, EStyleType::ELEMENT)) return true;
    return Command::AddAttribute(a);
}

void AddHSpace::AttributeNames(Csh &csh)
{
    Command::AttributeNames(csh);
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "label",
        "Enter some text the width of which will be used as horizontal spacing.",
        EHintType::ATTR_NAME));
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "space",
        "Enter a number in pixels to set horizontal spacing.",
        EHintType::ATTR_NAME));
    StringFormat::AttributeNames(csh, "text.");
}

bool AddHSpace::AttributeValues(std::string_view attr, Csh &csh)
{
    if (CaseInsensitiveEqual(attr,"label")) {
        return true;
    }
    if (CaseInsensitiveEqual(attr,"space")) {
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable() + "<number>",
            "Enter a number in pixels to set horizontal spacing.",
            EHintType::ATTR_VALUE, false));
        return true;
    }
    if (StringFormat::AttributeValues(attr, csh)) return true;
    if (Command::AttributeValues(attr, csh)) return true;
    return false;
}

ArcBase* AddHSpace::PostParseProcess(Canvas &/*canvas*/, bool /*hide*/,
                                     EntityRef&/*left*/, EntityRef&/*right*/,
                                     Numbering&/*number*/, MscElement** /*target*/,
                                     ArcBase* /*byref_target*/)
{
    if (!valid) return nullptr;
    if (!label && !space) {
        chart->Error.Error(file_pos.start, "You must specify either a numeric space or a label (or both) for the hspace command.");
        return nullptr;
    }
    //check if src and dst has disappeared
    src = chart->FindActiveParentEntity(src);
    dst = chart->FindActiveParentEntity(dst);
    //Substitute leftmost/rightmost child if user specified group entities
    if (src->children_names.size())
        src = chart->FindLeftRightDescendant(src, dst->pos < src->pos, true);
    if (dst->children_names.size())
        dst = chart->FindLeftRightDescendant(dst, src->pos < dst->pos, true);
    //we keep ourselves even if src/dst has disappeared
    return this;
}


void AddHSpace::Width(Canvas &canvas, DistanceRequirements &vdist)
{
    if (!valid) return;
    double dist = space.value_or(0); //0 if not specified by user
    if (label && label->length())
        dist += Label(*label, canvas, chart->Shapes, format).getTextWidthHeight().x;
    if (dist<0)
        chart->Error.Error(file_pos.start, "The horizontal space specified is negative. Ignoring it.");
    else
        vdist.InsertHSpace(src->index, dst->index, dist);
}


//////////////////////////////////////////////////////////////////////////////////

AddVSpace::AddVSpace(MscChart*msc)  :
    Command(EArcCategory::TINY_EFFORT, msc),
    format(msc->MyCurrentContext().text), compressable(false)
{
    draw_pass = EMscDrawPass::BEFORE_ENTITY_LINES;
}

bool AddVSpace::AddAttribute(const Attribute &a)
{
    if (a.Is("label")) {
        if (!a.CheckType(EAttrType::STRING, chart->Error)) return true;
        //EAttrType::CLEAR is OK above with value = ""
        label = a.value;
        return true;
    }
    if (a.Is("space")) {
        if (!a.CheckType(EAttrType::NUMBER, chart->Error)) return true;
        space = a.number;
        return true;
    }
    if (a.Is("compressable")) {
        if (!a.CheckType(EAttrType::BOOL, chart->Error)) return true;
        compressable = a.yes;
        return true;
    }
    if (format.AddAttribute(a, chart, EStyleType::ELEMENT)) return true;
    return Command::AddAttribute(a);
}

void AddVSpace::AttributeNames(Csh &csh)
{
    Command::AttributeNames(csh);
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "label",
        "Enter some text the height of which will be added as vertical empty space.",
        EHintType::ATTR_NAME));
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "space",
        "Enter a number in pixels to add that much empty vertical spacing.",
        EHintType::ATTR_NAME));
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "compressable",
        "Turn this on so that the vertical space added can be compressed away if the next element has 'compress' turned on or 'vspacing' set to 'compress'.",
        EHintType::ATTR_NAME));
    StringFormat::AttributeNames(csh, "text.");
}

bool AddVSpace::AttributeValues(std::string_view attr, Csh &csh)
{
    if (CaseInsensitiveEqual(attr,"label")) {
        return true;
    }
    if (CaseInsensitiveEqual(attr,"space")) {
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable() + "<number>",
            "Enter a number in pixels to add that much empty vertical spacing.",
            EHintType::ATTR_VALUE, false));
        return true;
    }
    if (CaseInsensitiveEqual(attr,"compressable")) {
        csh.AddYesNoToHints();
        return true;
    }
    if (StringFormat::AttributeValues(attr, csh)) return true;
    if (Command::AttributeValues(attr, csh)) return true;
    return false;
}

ArcBase* AddVSpace::PostParseProcess(Canvas &/*canvas*/, bool /*hide*/,
                                     EntityRef&/*left*/, EntityRef&/*right*/,
                                     Numbering&/*number*/, MscElement** /*target*/,
                                     ArcBase* /*byref_target*/)
{
    if (!valid) return nullptr;
    if (!label&& !space) {
        chart->Error.Error(file_pos.start, "You must specify either a numeric space or a label for the vspace command.");
        return nullptr;
    }
    return this;
}

//VSpaces do not have any cover - only mainline. This will make them
//allocate space correctly (even with compress) even if used inside a parallel
//block (there the mainlines affect only elements in the same column, whereas
//covers affect all - using the new parallel layout implemented in
//MscChart::PlaceArcLists().
void AddVSpace::Layout(Canvas &canvas, AreaList *cover)
{
    double dist = space.value_or(0);
    if (label && label->length())
        dist += Label(*label, canvas, chart->Shapes, format).getTextWidthHeight().y;
    if (dist<0)
        chart->Error.Error(file_pos.start, "The vertical space specified is negative. Ignoring it.");
    if (dist<=0)
        dist = 0;
    else if (!compressable) {
        Block b(chart->GetDrawing().x.from, chart->GetDrawing().x.till, 0, dist);
        area.mainline = area = b;
        area.arc = this;
        if (cover) {
            Area cover_area(this);
            cover_area.mainline = b;
            //nothing in "cover_area" just a mainline
            *cover = cover_area;
        }
    }
    height = dist;
}


//////////////////////////////////////////////////////////////////////////////////

ExtVertXPos::ExtVertXPos(std::string_view s, const FileLineColRange &sl, const VertXPos *p) :
VertXPos(p ? *p : VertXPos{}), side_line(sl)
{
    if (!valid) {
        side = BAD_SIDE;
        return;
    }
    if (CaseInsensitiveEqual(s, "left"))
        side = LEFT;
    else if (CaseInsensitiveEqual(s, "center"))
        side = CENTER;
    else if (CaseInsensitiveEqual(s, "right"))
        side = RIGHT;
    else
        side = BAD_SIDE;
}

ExtVertXPos::ExtVertXPos(ERelativeTo t, const FileLineColRange &sl, const VertXPos *p) :
VertXPos(p ? *p : VertXPos{}), side(t), side_line(sl)
{
    if (!valid)
        side = BAD_SIDE;
}


ExtVertXPos::ExtVertXPos(const VertXPos *p) :
VertXPos(p ? *p : VertXPos{}), side(CENTER), side_line()
{
    if (!valid) {
        side = BAD_SIDE;
        return;
    }
}

constexpr double Symbol::ellipsis_space_ratio;
constexpr double Symbol::def_arc_size;
constexpr double Symbol::def_rectangle_size;
constexpr double Symbol::def_shape_size;

bool Symbol::SetSymbolType(std::string_view symbol, const FileLineColRange& symbol_loc, bool text_at) {  //'text_at' is set if this comes from a 'text at' command as opposed to from a 'symbol text at' (different basic style)
    if (CaseInsensitiveEqual(symbol, "arc")) {
        SetStyleWithText("symbol", nullptr); //no refinement styles here, so we can set style in constructor
        symbol_type = ARC;
    } else if (CaseInsensitiveEqual(symbol, "rectangle")) {
        SetStyleWithText("symbol", nullptr);
        symbol_type = RECTANGLE;
    } else if (CaseInsensitiveEqual(symbol, "...")) {
        SetStyleWithText("symbol", nullptr);
        symbol_type = ELLIPSIS;
    } else if (CaseInsensitiveEqual(symbol, "shape")) {
        SetStyleWithText("symbol", nullptr);
        symbol_type = SHAPE;
    } else if (CaseInsensitiveEqual(symbol, "cross")) {
        SetStyleWithText("symbol", nullptr);
        symbol_type = CROSS;
    } else if (CaseInsensitiveEqual(symbol, "text")) {
        SetStyleWithText(text_at ? "text" : "symbol", nullptr);
        symbol_type = RECTANGLE;
        style.write().line.type = ELineType::NONE;
        style.write().fill.color = ColorType();
        style.write().fill.color2 = ColorType();
    } else if (!chart->languages) {
        chart->Error.Error(symbol_loc.start, "Unrecognized symbol type '" + std::string(symbol) + "'.",
                           "Use 'arc', '...', 'text', 'rectangle', 'cross' or 'shape'.");
        valid = false;
    } else if (!chart->languages->GetLanguage(symbol)) {
        chart->Error.Error(symbol_loc.start, "Unrecognized symbol type '" + std::string(symbol) + "'.",
                           StrCat("Use 'arc', '...', 'text', 'rectangle', 'cross', 'shape' or the name of a language: ",
                                  chart->languages->GetLanguageNames(), "."));
        valid = false;
    } else {
        SetStyleWithText("inlined_chart", nullptr);
        symbol_type = INLINE;
        symbol_type_loc = symbol_loc;
        inline_lang = symbol;
    }
    return valid;
}

/** Create symbol as a result of a 'symbol' command. */
Symbol::Symbol(MscChart*msc, std::string_view symbol, const FileLineColRange& symbol_loc, const FileLineColRange& l, const NamePair *enp,
                const ExtVertXPos *vxpos1, const ExtVertXPos *vxpos2) :
    LabelledArc(EArcCategory::SYMBOL, msc),
    hpos1(vxpos1 ? *vxpos1 : ExtVertXPos()),
    hpos2(vxpos2 ? *vxpos2 : ExtVertXPos()),
    vpos(enp ? *enp : NamePair({}, FileLineColRange(), {}, FileLineColRange())),
    gap1(chart->hscaleAutoXGap), gap2(chart->hscaleAutoXGap)
{
    //Set file_pos and symbol_type so that subsequent errors are OK, even if we set valid to false here.
    file_pos = l;
    if (!SetSymbolType(symbol, symbol_loc, false))
        return;
    if (!hpos1.entity1) {
        chart->Error.Error(file_pos.end, "Missing horizontal position. Ignoring symbol.");
        valid = false;
        return;
    }
    if (symbol_type == ELLIPSIS) {
        if (hpos1.side != ExtVertXPos::NONE && hpos2.side != ExtVertXPos::NONE) {
            chart->Error.Error(hpos2.e1line.start, "Symbol '...' can only have one vertical position indicated. Ignoring second one.");
            hpos2.side = ExtVertXPos::NONE;
        }
        if (vpos.dst.length()>0 && vpos.src.length()>0) {
            chart->Error.Error(vpos.dline.start, "Symbol '...' can only have one horizontal position indicated. Ignoring second one.");
            vpos.src.clear();
        }
        style.write().fill.color = *style.read().line.color;
    }
}


/** Determine for a text command, which has a single position (at clause),
 * what kind of symbol position to use (center at, left at or right at).*/
ExtVertXPos::ERelativeTo Symbol::CalcRelToForTextCommand(const VertXPos *vpos)
{
    if (!vpos)
        return ExtVertXPos::NONE;
    switch (vpos->pos) {
    default:
    case VertXPos::POS_THIRD_LEFT:
    case VertXPos::POS_THIRD_RIGHT:
        _ASSERT(0);
        FALLTHROUGH;
    case VertXPos::POS_AT:
    case VertXPos::POS_CENTER:
        return ExtVertXPos::CENTER;
    case VertXPos::POS_LEFT_BY:
    case VertXPos::POS_LEFT_SIDE:
        return ExtVertXPos::RIGHT;
    case VertXPos::POS_RIGHT_BY:
    case VertXPos::POS_RIGHT_SIDE:
        return ExtVertXPos::LEFT;
    }
}

//if equal, return -15, else their absolute difference
int mydiff(int a, int b, int bonus=-15) { return a == b ? bonus : abs(a - b); }

/** Return the arc in the catalog with the signature most similar to 'cat'.
 * Select the one with the exact same file_pos, if no such, then the one with the same line, 
 * else the one with the closest col and length.*/
InlineSignatureCatalog::iterator InlineSignature::WhichIsSimilar(InlineSignatureCatalog& cat) const {
    using T = InlineSignatureCatalog::value_type;
    if (auto exact_line_col = std::ranges::find_if(cat, [this](T& e) { return e.first.file_pos.start == file_pos.start; }); exact_line_col != cat.end())
        return exact_line_col;

    //Get all where file:line matches
    auto [from, till] = std::ranges::equal_range(cat, file_pos.start, [](const FileLineCol& A, const FileLineCol& b) { return std::tie(A.file, A.line) < std::tie(b.file , b.line); }, 
                                                 [](const T& A) ->const FileLineCol& { return A.first.file_pos.start; });
    //score them: diff in col=1, diff in lang = 10, diff in length=1.
    auto same_line = std::ranges::min_element(from, till, {}, [this](const T& A) { return mydiff(file_pos.start.col, A.first.file_pos.start.col, -10) 
                                                                                        + mydiff(content_len, A.first.content_len, -10)
                                                                                        + (lang==A.first.lang ? 0 : 10); });
    if (same_line != till) return same_line;
    _ASSERT(from == till); //empty - no line matches

    //score all the hints: equal col=-15, diff col=1; diff lang= 10, equal length=-15, length diff=1, line_diff = 2
    auto i = std::ranges::min_element(cat, {}, [this](const T& A) { return mydiff(file_pos.start.line, A.first.file_pos.start.line, 0) *2
                                                                         + mydiff(file_pos.start.col, A.first.file_pos.start.col, -15)
                                                                         + mydiff(content_len, A.first.content_len, -15)
                                                                         + (lang == A.first.lang ? 0 : 10); });
    return i;
}

std::string InlineSignature::Serialize() const {
    std::string s("0\n"); //version #0
    s << file_pos.Print() << ' ' << lang << ' ' << content_len << ' ';
    return s;
}

bool InlineSignature::Deserialize(std::string_view& s) {
    auto ver = DeserializeInt(s);
    if (!ver.first || ver.second > 0) return false;
    DeserializeLine(s); //eat the rest till the lineend
    if (s.length() == 0 || s.front() == 0) {
        _ASSERT(0);
        return false;
    }
    if (!file_pos.Read(s)) return false;
    DeserializeWhiteSpace(s);
    lang.clear();
    while (s.length() && std::isalnum(s.front())) {
        lang.push_back(s.front());
        s.remove_prefix(1);
    }
    if (auto [ok, len] = DeserializeInt(s); !ok || len < 0) return false;
    else content_len = len;
    DeserializeWhiteSpace(s);
    return true;
}



/** Serialize the signature to a string */
std::string InlineSignatureCatalog::Serialize() const {
    const size_t num = std::ranges::count_if(*this, [](const value_type& e) { return !e.second.empty(); });
    std::string s("0\n"); //version
    s << num << "\n"; //size
    for (auto& i : *this)
        if (!i.second.empty())
            s << i.first.Serialize() << ' ' << i.second.size() << ' ' << i.second;
    return s;
}

/** Deserialize the signature from a string. Sets 's' to the first char after us in the string.*/
bool InlineSignatureCatalog::Deserialize(std::string_view& s) {
    //always accept empty string
    if (s.length() == 0 || s.front() == 0) {
        clear();
        return true;
    }
    InlineSignatureCatalog cat;

    auto ver = DeserializeInt(s);
    if (!ver.first || ver.second != 0) return false;
    DeserializeLine(s); //eat the rest till the lineend
    if (s.length() == 0 || s.front() == 0) {
        _ASSERT(0);
        return false;
    }
    auto num = DeserializeInt(s);
    if (!num.first || num.second < 0) return false;
    DeserializeLine(s); //eat the rest till the lineend
    for (; s.length() && s.front() && num.second > 0; num.second--) {
        InlineSignature sig;
        if (!sig.Deserialize(s)) return false;
        DeserializeWhiteSpace(s);
        auto [ok, len] = DeserializeInt(s);
        if (!ok || len < 0) return false;
        if (!s.starts_with(' ')) return false;
        s.remove_prefix(1);
        if ((int)s.length() < len) return false;
        cat[sig] = s.substr(0, len);
        s.remove_prefix(len);
    }
    operator=(std::move(cat));
    return true;

}

/** Values for symbol position */
template<> const char EnumEncapsulator<ESymbolIndent>::names[][ENUM_STRING_LEN] =
{ "invalid", "left", "right", "center", "top", "bottom", "above_centerline", "centerline", "below_centerline", "" };


/** Create symbol as a result of a 'text at' command. */
Symbol::Symbol(MscChart*msc, std::string_view symbol, const FileLineColRange& symbol_loc, const FileLineColRange& l, const VertXPos *vpos, const FileLineColRange &at_pos) :
    LabelledArc(EArcCategory::SYMBOL, msc),
    hpos1(CalcRelToForTextCommand(vpos), at_pos, vpos),
    vpos({}, FileLineColRange(), {}, FileLineColRange()),
    gap1(0), gap2(chart->hscaleAutoXGap)
{
    file_pos = l;
    if (!SetSymbolType(symbol, symbol_loc, true))
        return;
    switch (hpos1.side) {
    case ExtVertXPos::LEFT: style.write().text.Apply("\\pl"); break;
    case ExtVertXPos::RIGHT: style.write().text.Apply("\\pr"); break;
    case ExtVertXPos::CENTER: style.write().text.Apply("\\pc"); break;
    default: valid=false; //can happen with an invalid (null) vpos
    }
}

/** Create a symbol using positions above/below/leftof/rightof/around. */
Symbol::Symbol(MscChart* msc, std::string_view symbol, const FileLineColRange& symbol_loc, const FileLineColRange& l,
               std::string_view position, const FileLineColRange& pos_loc, std::string_view refname, const FileLineColRange& ref_loc) :
    LabelledArc(EArcCategory::SYMBOL, msc),
    pos_refname(refname), pos_refname_loc(ref_loc.start),
    vpos({}, FileLineColRange(), {}, FileLineColRange()) 
{
    file_pos = l;
    if (!Convert(position, pos_type)) {
        chart->Error.Error(pos_loc.start, StrCat("Internal error: unknown position specifier: '", position, "'. Ignoring symbol."),
                           StrCat("Use one of ", CandidatesFor<ESymbolPosType>(), "."));
        valid = false;
        return;
    }
    if (!SetSymbolType(symbol, symbol_loc, true))
        return;
}
static constexpr double _ellipsis_sizes[] = { 0, 0.5, 1, 1.5, 3, 5, 8, 15 }; //INVALID, EPSILON, SPOT, TINY, SMALL, NORMAL, LARGE, ENORMOUS
constexpr double EllipsisScale(EArrowSize s) noexcept { return _ellipsis_sizes[static_cast<int>(s)]; }


bool Symbol::AddAttribute(const Attribute &a)
{
    if (a.Is("xsize") || a.Is("ysize")) {
        if (pos_type == AROUND) {
            chart->Error.Error(a, false, StrCat("Attribute '", a.name, "' is not applicable, when the symbol is 'around' another element.", "Try using 'adjust' instead."));
        } else {
            if (a.type == EAttrType::STRING && IsByRef()) {
                if (CaseInsensitiveEqual(a.value, "auto")) {
                    (a.Is("xsize") ? xsize : ysize) = -1; //negative number means 'span to the size of the referenced element'
                    (a.Is("xsize") ? xsize : ysize).file_pos = a.linenum_value.start;
                } else
                    chart->Error.Error(a, true, StrCat("Attribute '", a.name, "' can take a positive number or 'auto'."));

            } else if (a.CheckType(EAttrType::NUMBER, chart->Error)) {
                if (a.number >= 0) {
                    (a.Is("xsize") ? xsize : ysize) = a.number;
                    (a.Is("xsize") ? xsize : ysize).file_pos = a.linenum_value.start;
                } else
                    chart->Error.Error(a, true, StrCat("Attribute '", a.name, "' can take a positive number", IsByRef() ? " or 'auto'." : "."));
            }
        }
        return true;
    }
    if (a.Is("size")) {
        if (pos_type == AROUND) {
            chart->Error.Error(a, false, StrCat("Attribute '", a.name, "' is not applicable, when the symbol is 'around' another element.", "Try using 'adjust' instead."));
        } else if (a.type == EAttrType::NUMBER && 0.001 <= a.number && a.number <= 100) {
            size = a.number;
            size.file_pos = a.linenum_attr.start;
        } else if (EArrowSize s; a.type == EAttrType::STRING && Convert(a.value, s)) {
            size = symbol_type == ELLIPSIS ? EllipsisScale(s) : ArrowScale(s);
            size.file_pos = a.linenum_attr.start;
        } else
            chart->Error.Error(a, true, StrCat("The 'size' attribute must be one of '", CandidatesFor<EArrowSize>("', '"), "', or a number between 0.001 and 100. Ignoring it."));
        return true;
    }
    if (a.Is("align")) {
        if (!IsByRef()) 
            chart->Error.Error(a, false, "The 'align' attribute is only applicable for symbols positioned in reference to another element (using 'above', 'below', 'leftof' or 'rightof'). Ignoring it.");
        else if (pos_type == AROUND)
            chart->Error.Error(a, false, "The 'align' attribute is not applicable for symbols 'around' their referenced element. Ignoring it.");
        else switch (pos_type) {
        case ABOVE:
        case BELOW:
            if (ESymbolIndent i; Convert(a.value, i) && i <= ESymbolIndent::MAX_HORIZONTAL) {
                pos_indent = i;
                pos_indent.file_pos = a.linenum_value.start;
            } else
                chart->Error.Error(a, true, "Invalid alignment. Use one of '"+ CandidatesFor(i, "', '", ESymbolIndent(1), ESymbolIndent::MAX_HORIZONTAL) + "'.");
            break;
        case RIGHTOF:
        case LEFTOF:
            if (ESymbolIndent i; Convert(a.value, i) && ESymbolIndent::MAX_HORIZONTAL <= i) {
                pos_indent = i;
                pos_indent.file_pos = a.linenum_value.start;
            } else
                chart->Error.Error(a, true, "Invalid alignment. Use one of '" + CandidatesFor(i, "', '", ESymbolIndent::MAX_HORIZONTAL) + "'.");
            break;
        case AROUND:
            chart->Error.Error(a, false, "The 'align' attribute is not applicable for 'around' symbols. Ignoring it.");
            break;
        default:
        case INVALID:
            return false;
        }
        return true;
    }
    if (a.Is("distance")) {
        if (!IsByRef()) 
            chart->Error.Error(a, false, "The 'distance' attribute is only applicable for symbols positioned in reference to another element (using 'above', 'below', 'leftof', 'rightof' or 'around'). Ignoring it.");
        else if (a.CheckType(EAttrType::NUMBER, chart->Error))
            pos_distance = a.number;
        return true;
    }
    if (a.Is("offset")) {
        if (!IsByRef()) 
            chart->Error.Error(a, false, "The 'offset' attribute is only applicable for symbols positioned in reference to another element (using 'above', 'below', 'leftof' or 'rightof'). Ignoring it.");
        else if (pos_type==AROUND)
            chart->Error.Error(a, false, "The 'offset' attribute is not applicable for symbols 'around' their referenced element. Ignoring it.");
        else if (a.CheckType(EAttrType::NUMBER, chart->Error))
            pos_offset = a.number;
        return true;
    }
    if (a.Is("adjust")) {
        if (!IsByRef()) 
            chart->Error.Error(a, false, "The 'offset' attribute is only applicable for symbols positioned in reference to another element (using 'above', 'below', 'leftof' or 'rightof'). Ignoring it.");
        else if (a.type == EAttrType::NUMBER) {
            pos_adjust.x.from = pos_adjust.x.till = pos_adjust.y.from = pos_adjust.y.till = a.number;
            pos_adjust_loc = a.linenum_value.start;
        } else {
            int pos[4] = { -1, -1, -1, -1 };
            Block tmp = { 0,0,0,0 };
            int num = sscanf(a.value.c_str(), "%lf%n,%lf%n,%lf%n,%lf%n",
                             &tmp.x.from, pos, &tmp.x.till, pos + 1,
                             &tmp.y.from, pos + 2, &tmp.y.till, pos + 3);
            if (num == 0 || pos[num - 1] != (int)a.value.size())
                chart->Error.Error(a, true, "The 'adjust' takes either one number (applying to all 4 sides) or 2-4 numbers (applying to left/right/top/bottom) separated by commas and not this. Ignoring it.");
            else {
                pos_adjust = tmp;
                pos_adjust_loc = a.linenum_value.start;
            }
        }
        return true;
    }
    if (a.Is("draw_time")) {  //We add this even though it is in ArcBase::AddAttribute, but Command::AddAttribute does not call that
        if (!a.EnsureNotClear(chart->Error, EStyleType::ELEMENT)) return true;
        if (a.type == EAttrType::STRING && Convert(a.value, draw_pass)) return true;
        a.InvalidValueError(CandidatesFor(draw_pass), chart->Error);
        return true;
    }
    if (a.Is("label") && (symbol_type == ELLIPSIS || symbol_type == CROSS)) {
        chart->Error.Error(a, false, "Labels cannot be specified for '...' and 'cross' symbols. Ignoring it.");
        return true;
    }
    if (a.Is("label") && symbol_type == INLINE) {
        chart->Error.Error(a, false, "Labels cannot be specified inlined charts. Ignoring it.");
        return true;
    }
    if (a.Is("shape") && symbol_type != SHAPE) {
        chart->Error.Error(a, false, "Attribute 'shape' can only be applied to 'shape' symbols. Ignoring it.");
        return true;
    }
    if (a.Is("shape.size"))
        return false;
    if (a.Is("layout_warning")) {
        if (a.EnsureNotClear(chart->Error, EStyleType::ELEMENT)
            && a.CheckType(EAttrType::BOOL, chart->Error))
            layout_warning = a.yes;
        return true;
    }
    return LabelledArc::AddAttribute(a);
}

//kind is 
// - 0 for symbols positioned via markers and entities
// - 1 for symbols positioned by reference to other elements
// - 2 if unknown
void Symbol::AttributeNames(Csh &csh, int kind)
{
    const char* add_auto = kind ? " Use 'auto' for the width of the referenced element." : "";
    LabelledArc::AttributeNames(csh);
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "xsize",
        StrCat("Set the width of the symbol in pixels.", add_auto),
        EHintType::ATTR_NAME));
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "ysize",
        StrCat("Set the height of the symbol in pixels.", add_auto),
        EHintType::ATTR_NAME));
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "size",
        "Set the size of the symbol.",
        EHintType::ATTR_NAME));
    csh.AttributeNamesForStyle("symbol");
    if (kind) {
        csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "align",
            "Shall the symbol align with left/top, right/bottom or the center of the referenced element.",
            EHintType::ATTR_NAME));
        csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "distance",
            "Extra distance between the referenced element and the symbol. Can be negative.",
            EHintType::ATTR_NAME));
        csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "offset",
            "Shift 'above'/'below' the symbols right; 'leftof'/'rightof' symbols down by this many pixels. Can be negative.",
            EHintType::ATTR_NAME));
        csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "adjust",
            "Adjust the left/right/top/bottom border of the element effectively changing its size.",
            EHintType::ATTR_NAME));
    }
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME)+"layout_warning",
        "Set this to 'no' to suppress a layout warning, when the vertical is attached to a parallel block series with "
        "'layout=one_by_one_merge' (which is the default layout) and the vertical extends above the parallel block series.",
        EHintType::ATTR_NAME));
}


bool CshHintGraphicCallbackForSymbolAlign(Canvas* canvas, CshHintGraphicParam p, CshHintStore&) {
    if (!canvas) return false;
    int t = (int)p; //0 left, 1 right, 2 center, 3 top, 4 bottom
    const LineAttr elem_line(ELineType::SOLID, ColorType::black(), 1, ECornerType::NONE, 0);
    const FillAttr elem_fill(ColorType::lgray(), EGradientType::NONE);
    Block elem(HINT_GRAPHIC_SIZE_X / 3., HINT_GRAPHIC_SIZE_X * 2 / 3., HINT_GRAPHIC_SIZE_Y / 3., HINT_GRAPHIC_SIZE_Y * 2 / 3.);
    canvas->Fill(elem, elem_fill);
    canvas->Line(elem, elem_line);

    const LineAttr line(ELineType::SOLID, ColorType::red(), 1, ECornerType::NONE, 0);
    std::vector<Block> symb = [t]() -> std::vector<Block> { constexpr double s = 1/8., e = 1/3.; switch (t) { //s the width/height of the symbol, e is the space left and right of the referenced element
    case 0: /*LEFT*/   return { Block(e, e + s, e - s, e), Block(e, e + s, 1 - e, 1 - e + s) };
    case 1: /*RIGHT*/  return { Block(1 - e - s, 1 - e, e - s, e), Block(1 - e - s, 1 - e, 1 - e, 1 - e + s) };
    case 2: /*CENTER*/ return { Block(0.5 - s / 2, 0.5 + s / 2, e - s, e), Block(0.5 - s / 2, 0.5 + s / 2, 1 - e, 1 - e + s),
                                Block(e - s, e, 0.5 - s / 2, 0.5 + s / 2), Block(1 - e, 1 - e + s, 0.5 - s / 2, 0.5 + s / 2) };
    case 3: /*TOP*/    return { Block(e - s, e, e, e + s), Block(1 - e, 1 - e + s, e, e + s) };
    case 4: /*BOTTOM*/ return { Block(e - s, e, 1 - e, 1 - e + s), Block(1 - e, 1 - e + s, 1 - e, 1 - e + s) };;
    default: return {};
    }}();
    if (symb.empty()) return false;
    for (Block& b : symb) {
        b.Scale(XY(HINT_GRAPHIC_SIZE_X, HINT_GRAPHIC_SIZE_Y));
        canvas->Line(Contour(b.Centroid(), b.x.Spans()/2, b.y.Spans()/2), line);
    }
    return true;
}


bool Symbol::AttributeValues(std::string_view attr, Csh& csh, int kind) {
    if (CaseInsensitiveEqual(attr, "xsize")) {
        if (kind)
            csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "auto",
                "Set the width of the symbol to that of the referenced element. (Use 'adjust' to make changes to it.)",
                EHintType::ATTR_VALUE));
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable() + "<number>",
            "Set the width of the symbol in pixels.",
            EHintType::ATTR_VALUE, false));
        return true;
    }
    if (CaseInsensitiveEqual(attr, "ysize")) {
        if (kind)
            csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "auto",
                "Set the height of the symbol to that of the referenced element. (Use 'adjust' to make changes to it.)",
                EHintType::ATTR_VALUE));
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable() + "<number>",
            "Set the height of the symbol in pixels.",
            EHintType::ATTR_VALUE, false));
        return true;
    }
    if (CaseInsensitiveEqual(attr, "size")) {
        FullArrowAttr::AttributeValues(attr, csh, EArcArrowType::ANY);
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable() + "<number>",
            "A relative scaling factor, defaults to 0.5.",
            EHintType::ATTR_VALUE, false));
        return true;
    }
    if (kind) {
        if (CaseInsensitiveEqual(attr, "align")) {
            csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "left",
                "Align the symbol to the left side of the referenced element. Valid only for 'above' or 'below' placements.",
                EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForSymbolAlign, 0U, 0));
            csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "right",
                "Align the symbol to the right side of the referenced element. Valid only for 'above' or 'below' placements.",
                EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForSymbolAlign, 1U, 1));
            csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "left",
                "Align the symbol to the center of the referenced element. Valid for 'above'/'below' and 'leftof'/'rightof' placements.",
                EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForSymbolAlign, 2U, 2));
            csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "top",
                "Align the symbol to the top side of the referenced element. Valid only for 'leftof' or 'rightof' placements.",
                EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForSymbolAlign, 3U, 3));
            csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "bottom",
                "Align the symbol to the bottom side of the referenced element. Valid only for 'leftof' or 'rightof' placements.",
                EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForSymbolAlign, 4U, 4));
            return true;
        }
        if (CaseInsensitiveEqual(attr, "distance")) {
            csh.AddToHints(CshHint(csh.HintPrefixNonSelectable() + "<number>",
                "The number of extra pixels between the symbol and the referenced element. Can be negative. "
                "For 'above'/'below' symbols this moves the symbol up/down, whereas for 'leftof'/'rightof' symbols this moves the symbol left/right, when positive.",
                EHintType::ATTR_VALUE, false));
            return true;
        }
        if (CaseInsensitiveEqual(attr, "offset")) {
            csh.AddToHints(CshHint(csh.HintPrefixNonSelectable() + "<number>",
                "Shift the symbol right (for 'above'/'below') or down (for 'leftof'/'rightof') by this many pixels. Can be negative.",
                EHintType::ATTR_VALUE, false));
            return true;
        }
        if (CaseInsensitiveEqual(attr, "adjust")) {
            csh.AddToHints(CshHint(csh.HintPrefixNonSelectable() + "<left>,<right>,<top>,<bottom>",
                "Adjust the boundaries of the symbol's bounding box, effectively changing its size. "
                "Specify 4 numbers, one for the left/right/top/bottom side in pixels. "
                "Positive numbers increase the size of the symbol, negative ones reduce it.",
                EHintType::ATTR_VALUE, false));
            csh.AddToHints(CshHint(csh.HintPrefixNonSelectable() + "<left>,<right>,<top>",
                "Adjust the boundaries of the symbol's bounding box, effectively changing its size. "
                "Specify 3 numbers, one for the left/right/top side in pixels (no change to bottom). "
                "Positive numbers increase the size of the symbol, negative ones reduce it.",
                EHintType::ATTR_VALUE, false));
            csh.AddToHints(CshHint(csh.HintPrefixNonSelectable() + "<left>,<right>",
                "Adjust the boundaries of the symbol's bounding box, effectively changing its size. "
                "Specify 2 numbers, one for the left/right side in pixels (no change to top/bottom). "
                "Positive numbers increase the size of the symbol, negative ones reduce it.",
                EHintType::ATTR_VALUE, false));
            csh.AddToHints(CshHint(csh.HintPrefixNonSelectable() + "<number>",
                "Adjust the boundaries of the symbol's bounding box, effectively changing its size. "
                "The single number specified will apply to all 4 sides, effectively expanding (positive number) or shrinking (negative number) the symbol.",
                EHintType::ATTR_VALUE, false));
            return true;
        }
    }
    if (CaseInsensitiveEqual(attr, "layout_warnings")) {
        csh.AddYesNoToHints();
        return true;
    }
    if (LabelledArc::AttributeValues(attr, csh)) return true;
    if (csh.AttributeValuesForStyle(attr, "symbol")) return true;
    return false;
}

void Symbol::AddInline(std::string_view inline_text, const FileLineColRange& pos) {
    //fill in rest of signature
    signature = { .file_pos = pos, .lang = std::string{inline_lang}, .content_len = (unsigned)inline_text.size() };

    const auto force = signature.WhichIsSimilar(chart->inline_gui_state); //use the GUI state stored for us
    inlining_data.chart = chart->CompileInlineText(inline_lang, symbol_type_loc, inline_text, pos.start, this,
                                                   force != chart->inline_gui_state.end() ? force->second : "");
    inlining_data.file_pos = pos;
    if (inlining_data.chart) {
        if (force != chart->inline_gui_state.end())
            if (std::string new_gui_state = inlining_data.chart->SerializeGUIState(); new_gui_state.size()) {
                chart->inline_gui_state_instead[signature] = std::move(new_gui_state);
                chart->inline_gui_state.erase(force);
            }
    } else
        valid = false;
}


ArcBase* Symbol::PostParseProcess(Canvas &canvas, bool hide, EntityRef &left, EntityRef &right,
                                  Numbering& number, MscElement** target, ArcBase* byref_target)
{
    if ((xsize || ysize) && size) {
        chart->Error.Error(size.file_pos, "Use only either 'size' or 'xsize'/'ysize'. Ignoring 'size'.");
        size.reset();
    }

    if (IsByRef()) {
        if (pos_refname.size()) {
            const auto i = chart->ReferenceNames.find(pos_refname);
            if (i == chart->ReferenceNames.end()) {
                chart->Error.Error(pos_refname_loc, StrCat("Cannot find element with refname of '", pos_refname, "'. Ignoring symbol."));
                return nullptr;
            }
            pos_reftarget = i->second.arc;
            if (!pos_reftarget->CanBeAlignedTo()) {
                chart->Error.Error(pos_refname_loc, StrCat("The target element with refname of '", pos_refname, "' cannot be aligned to. Ignoring symbol."));
                chart->Error.Error(pos_reftarget->file_pos.start, pos_refname_loc, "Here is the referenced element.");
                return nullptr;
            }
        } else if (byref_target == nullptr) {
            valid = false;
            chart->Error.Error(file_pos.start, "This symbol has no reference element and no prior element to align to. Ignoring it.",
                               StrCat("Either specify a reference name after '", PrintEnum(pos_type), "' or "
                                      "specify it directly after another element to align to."));
            return nullptr;
        } else
            pos_reftarget = byref_target;
        _ASSERT(pos_reftarget);
        _ASSERT(pos_reftarget->CanBeAlignedTo());
        if (ESymbolIndent::ABOVE_CENTERLINE <= pos_indent.value_or(ESymbolIndent::CENTER) && !pos_reftarget->CanJoin()) {
            switch (pos_indent.value()) {
            case ESymbolIndent::ABOVE_CENTERLINE: pos_indent = ESymbolIndent::TOP; break;
            case ESymbolIndent::CENTERLINE: pos_indent = ESymbolIndent::CENTER; break;
            case ESymbolIndent::BELOW_CENTERLINE: pos_indent = ESymbolIndent::BOTTOM; break;
            case ESymbolIndent::INVALID:
            case ESymbolIndent::LEFT:
            case ESymbolIndent::RIGHT:
            case ESymbolIndent::CENTER:
            case ESymbolIndent::TOP:
            case ESymbolIndent::BOTTOM:
                break;
            }
            chart->Error.Error(pos_indent.file_pos, "The referred element has no centerline (only boxes and arrows have). Using '" + PrintEnum(*pos_indent) + "' instead.");
            chart->Error.Error(pos_reftarget->file_pos.start, pos_indent.file_pos, "Here is the referenced element.");
        }
    } 
    *target = this; 
    if (!valid) return nullptr;

    if (symbol_type == INLINE && label.size()) {
        chart->Error.Error(linenum_label, "Cannot display a label for inlined charts. Ignoring it.");
        label = {};
    }
    if (!IsByRef()) {
        if (vpos.src.length()) {
            auto m = chart->GetMarker(vpos.src);
            if (!m) {
                chart->Error.Error(file_pos.start, "Cannot find marker '" + StringFormat::ConvertToPlainTextCopy(vpos.src) + "'."
                                   " Ignoring symbol.");
                return nullptr;
            }
        }
        if (vpos.dst.length()) {
            auto m = chart->GetMarker(vpos.dst);
            if (!m) {
                chart->Error.Error(file_pos.start, "Cannot find marker '" + StringFormat::ConvertToPlainTextCopy(vpos.dst) + "'."
                                   " Ignoring symbol.");
                return nullptr;
            }
        }
        if (hpos1.side == ExtVertXPos::BAD_SIDE) {
            chart->Error.Error(hpos1.side_line.start, "Invalid horizontal position designator. Ignoring symbol.", "Use 'left', 'right' or 'center'.");
            return nullptr;
        }
        if (hpos2.valid && hpos2.side == ExtVertXPos::BAD_SIDE) {
            chart->Error.Error(hpos2.side_line.start, "Invalid horizontal position designator. Ignoring it.", "Use 'left', 'right' or 'center'.");
            hpos2.valid = false;
        }
        if (hpos1.side == ExtVertXPos::NONE) {
            chart->Error.Error(file_pos.end, "You need to specify at least one horizontal position designator. Ignoring symbol.", "Use 'left at', 'right at' or 'center at'.");
            return nullptr;
        }
        if (hpos2.valid && hpos1.side == hpos2.side) {
            chart->Error.Error(hpos2.side_line.start, "You cannot specify the same horizontal position designator twice. Ignoring the second one.");
            chart->Error.Error(hpos1.side_line.start, hpos2.side_line.start, "Here is the first designator.");
            hpos2.valid = false;
        }
        if (chart->ErrorIfEntityGrouped(hpos1.entity1, hpos1.e1line.start)) return nullptr;
        if (chart->ErrorIfEntityGrouped(hpos1.entity2, hpos1.e2line.start)) return nullptr;
        if (hpos2.valid && chart->ErrorIfEntityGrouped(hpos2.entity1, hpos2.e1line.start)) return nullptr;
        if (hpos2.valid && chart->ErrorIfEntityGrouped(hpos2.entity2, hpos2.e2line.start)) return nullptr;
    }
    if (symbol_type == SHAPE) {
        if (!style.read().shape) {
            chart->Error.Error(file_pos.start, "Missing 'shape' attribute for a shape symbol. Ignoring symbol.");
            return nullptr;
        }
        if (*style.read().shape<0) {//unrecognized shape
            chart->Error.Error(file_pos.start, "I need a valid 'shape' attribute for a shape symbol. Ignoring symbol.");
            return nullptr; //Error already given in AddAttribute
        }
        if (label.length() && chart->Shapes[*style.read().shape].GetLabelPos(Block(0,10,0,10)).IsInvalid()) {
            chart->Error.Error(file_pos.start, "Shape '"+chart->Shapes[*style.read().shape].name+"' takes no label. Ignoring 'label' attribute.");
            label.clear();
        }
    }
    if (hide) return nullptr;

    if (!IsByRef()) {
        hpos1.entity1 = chart->FindActiveParentEntity(hpos1.entity1);
        hpos1.entity2 = chart->FindActiveParentEntity(hpos1.entity2);
        if (hpos2.valid) {
            hpos2.entity1 = chart->FindActiveParentEntity(hpos2.entity1);
            hpos2.entity2 = chart->FindActiveParentEntity(hpos2.entity2);
        } else
            hpos2.side = ExtVertXPos::NONE;
        if (hpos1.side > hpos2.side) std::swap(hpos1, hpos2);
    }
    switch (symbol_type) {
    case ARC:
        if (!IsByRef()) {
            if (hpos2.side == ExtVertXPos::NONE && !xsize.has_value())
                xsize = def_arc_size * 2 * size.value_or(ArrowScale(EArrowSize::SMALL));
            if ((vpos.dst.length() == 0 || vpos.src.length() == 0) && !ysize.has_value())
                ysize = def_arc_size * 2 * size.value_or(ArrowScale(EArrowSize::SMALL));
        } //For symbols positioned relative to another entity we leave xsize and ysize unspecified, if not set by the user
        break;
    case INLINE:
        if (inlining_data.chart) {
            chart->Error += inlining_data.chart->Error;
            //Drop chart if too small
            if (inlining_data.chart->GetTotal().Spans().x < 1 || inlining_data.chart->GetTotal().Spans().y < 1)
                inlining_data.chart.reset();
        }
        if (!inlining_data.chart) return nullptr; //too small or erroneous chart will not be shown
        FALLTHROUGH;
    case RECTANGLE:
        break; //leave xsize and ysize unset if not set by the user
    case ELLIPSIS:
        if (!xsize && !size) {
            xsize = size.value_or(EllipsisScale(EArrowSize::SMALL));
            ysize = *xsize * (3 + 2 * ellipsis_space_ratio);
        } else if (!ysize) 
            ysize = *xsize * (3 + 2 * ellipsis_space_ratio);
        else if (!xsize)
            xsize = *ysize / (3 + 2 * ellipsis_space_ratio);
        //else both xsize & ysize set by user.
        break;
    case CROSS:
        if (!xsize && !ysize)
            xsize = ysize = SingleArrowHead::baseArrowWidth * size.value_or(ArrowScale(EArrowSize::SMALL));
        else if (!ysize)
            ysize = *xsize;
        else if (!xsize)
            xsize = *ysize;
        //else both xsize & ysize set by user.
        break;
    case SHAPE:
        XY s = chart->Shapes[*style.read().shape].GetMax().Spans();
        if (!IsByRef()) {
            if (hpos2.side == ExtVertXPos::NONE && !xsize.has_value()) {
                if ((vpos.dst.length() == 0 || vpos.src.length() == 0) && !ysize.has_value()) {
                    //both x and y size unspecified - Set them so that the shape fits into a 50x50
                    if (s.x < s.y) {
                        ysize = def_shape_size * 2 * size.value_or(ArrowScale(EArrowSize::SMALL));
                        //fallthrough to calc xsize
                    } else {
                        xsize = def_shape_size * 2 * size.value_or(ArrowScale(EArrowSize::SMALL));
                        goto set_y;
                    }
                }
                if (ysize)
                    xsize = s.x / s.y * *ysize;
            } else if ((vpos.dst.length() == 0 || vpos.src.length() == 0)
                       && !ysize.has_value() && xsize.has_value()) {
            set_y:
                ysize = s.y / s.x * *xsize;
            }
        } else {
            if (!xsize.has_value()) {
                if (!ysize.has_value()) {
                    //both x and y size unspecified - Set them so that the shape fits into a 50x50
                    if (s.x < s.y) {
                        ysize = def_shape_size * 2 * size.value_or(ArrowScale(EArrowSize::SMALL));
                        //fallthrough to calc xsize
                    } else {
                        xsize = def_shape_size * 2 * size.value_or(ArrowScale(EArrowSize::SMALL));
                        goto set_y2;
                    }
                }
                if (ysize)
                    xsize = s.x / s.y * *ysize;
            } else if (!ysize.has_value() && xsize.has_value()) {
            set_y2:
                ysize = s.y / s.x * *xsize;
            }
        }
        break;
    }
    if (!IsByRef()) {
        act_size1 = hpos1.entity1->GetRunningWidth(chart->activeEntitySize) / 2;
        if (hpos2.side != ExtVertXPos::NONE)
            act_size2 = hpos2.entity1->GetRunningWidth(chart->activeEntitySize) / 2;
    }
    return LabelledArc::PostParseProcess(canvas, hide, left, right, number, target, byref_target);
}

void Symbol::Width(Canvas &, DistanceRequirements &vdist)
{
    if (!IsByRef()) {
        if (symbol_type == RECTANGLE) {
        //set sizes now, if not yet set - label is now final
            if (hpos2.side == ExtVertXPos::NONE && !xsize.has_value())
                xsize = parsed_label.size() ? parsed_label.getTextWidthHeight().x : def_rectangle_size;
            if ((vpos.dst.length() == 0 || vpos.src.length() == 0) && !ysize.has_value())
                ysize = parsed_label.size() ? parsed_label.getTextWidthHeight().y : def_rectangle_size;
        } else if (symbol_type == INLINE) {
            if (hpos2.side == ExtVertXPos::NONE && !xsize.has_value())
                xsize = inlining_data.chart->GetTotal().x.Spans() * 2 * size.value_or(ArrowScale(EArrowSize::SMALL));
            // ysize may still be unset even if not both vpos.src and vpos.dst are set
        }
    } else { //for relative positioned symbols 
        if (symbol_type == RECTANGLE || symbol_type == ARC) {
            if (!xsize.has_value())
                xsize = (parsed_label.size() ? parsed_label.getTextWidthHeight().x : def_rectangle_size) * 2 * size.value_or(ArrowScale(EArrowSize::SMALL));
            if (!ysize.has_value())
                ysize = (parsed_label.size() ? parsed_label.getTextWidthHeight().y : def_rectangle_size) * 2 * size.value_or(ArrowScale(EArrowSize::SMALL));
        } else if (symbol_type == INLINE)
            if (!xsize.has_value())
                xsize = inlining_data.chart->GetTotal().x.Spans() * 2 * size.value_or(ArrowScale(EArrowSize::SMALL));
        _ASSERT(xsize);
    }

    //Add a new element to vdist
    vdist.AddElementTop(this);
    //Add activation status right away
    AddEntityLineWidths(vdist);

    if (!IsByRef()) {
        if (*style.read().makeroom) {
            // Make room only if we have only one pos specified or we have two pos, one left and one right
            if (hpos2.side == ExtVertXPos::NONE || (hpos1.side != ExtVertXPos::CENTER && hpos2.side != ExtVertXPos::CENTER)) {
                //First seee to the case with one position specifier only
                const unsigned index = hpos1.entity1->index; //index of the (first) entity
                double off = hpos1.pos == VertXPos::POS_LEFT_BY ? -gap1-act_size1-gap2 :
                             hpos1.pos == VertXPos::POS_RIGHT_BY ? +gap1+act_size1+gap2 :
                             hpos1.pos == VertXPos::POS_LEFT_SIDE ? -gap1-act_size1 :
                             hpos1.pos == VertXPos::POS_RIGHT_SIDE ? +gap1+act_size1 : 0;
                off += hpos1.offset;

                //vpos can have one of its strings as MARKER_HERE_STR (and the other a valid marker)
                //or if all markers omitted, both can be nullptr. In the latter case we are inline
                //and shall only take the last segment (GetIteratorLast() -> GetIteratorEnd()).
                const auto si = vpos.src==MARKER_HERE_STR ? vdist.GetIteratorEnd() :
                                vpos.src.length()==0 ? vdist.GetIteratorLast() :
                                    vdist.GetIterator(vpos.src, true);       //we assume src is higher than dst
                const auto di = vpos.dst==MARKER_HERE_STR ? vdist.GetIteratorEnd() :
                                vpos.src.length()==0 ? vdist.GetIteratorEnd() :
                                    vdist.GetIterator(vpos.dst, false);
                if (hpos2.side == ExtVertXPos::NONE) {
                    const double width = *xsize; //must be valid if only one pos specifier
                    switch (hpos1.side) {
                    case ExtVertXPos::CENTER:
                        switch (hpos1.pos) {
                        default:
                        case VertXPos::POS_THIRD_LEFT:
                        case VertXPos::POS_THIRD_RIGHT:
                            _ASSERT(0); FALLTHROUGH; //these should not appear here, used only for loss
                        case VertXPos::POS_RIGHT_BY:
                        case VertXPos::POS_LEFT_BY:
                        case VertXPos::POS_LEFT_SIDE:
                        case VertXPos::POS_RIGHT_SIDE:
                        case VertXPos::POS_AT:
                            vdist.Insert(index, DISTANCE_LEFT, width/2 - off, si, di);
                            vdist.Insert(index, DISTANCE_RIGHT, width/2 + off, si, di);
                            vdist.InsertEntity(hpos1.entity1);
                            break;
                        case VertXPos::POS_CENTER:
                            vdist.Insert(index, hpos1.entity2->index, width + off, si, di);
                            vdist.InsertEntity(hpos1.entity1);
                            vdist.InsertEntity(hpos1.entity2);
                            break;
                        }
                        break;
                    case ExtVertXPos::LEFT:
                        switch (hpos1.pos) {
                        default:
                        case VertXPos::POS_THIRD_LEFT:
                        case VertXPos::POS_THIRD_RIGHT:
                            _ASSERT(0); FALLTHROUGH; //these should not appear here, used only for loss
                        case VertXPos::POS_RIGHT_BY:
                        case VertXPos::POS_LEFT_BY:
                        case VertXPos::POS_RIGHT_SIDE:
                        case VertXPos::POS_LEFT_SIDE:
                        case VertXPos::POS_AT:
                            vdist.Insert(index, DISTANCE_RIGHT, width + off, di, si);
                            vdist.InsertEntity(hpos1.entity1);
                            break;
                        case VertXPos::POS_CENTER:
                            vdist.Insert(index, hpos1.entity2->index, 2*(width + off), si, di);
                            vdist.InsertEntity(hpos1.entity1);
                            vdist.InsertEntity(hpos1.entity2);
                            break;
                        }
                        break;
                    case ExtVertXPos::RIGHT:
                        switch (hpos1.pos) {
                        default:
                        case VertXPos::POS_THIRD_LEFT:
                        case VertXPos::POS_THIRD_RIGHT:
                            _ASSERT(0); FALLTHROUGH; //these should not appear here, used only for loss
                        case VertXPos::POS_RIGHT_BY:
                        case VertXPos::POS_LEFT_BY:
                        case VertXPos::POS_RIGHT_SIDE:
                        case VertXPos::POS_LEFT_SIDE:
                        case VertXPos::POS_AT:
                            vdist.Insert(index, DISTANCE_LEFT, width - off, di, si);
                            vdist.InsertEntity(hpos1.entity1);
                            break;
                        case VertXPos::POS_CENTER:
                            vdist.Insert(index, hpos1.entity2->index, 2*(width + off), si, di);
                            vdist.InsertEntity(hpos1.entity1);
                            vdist.InsertEntity(hpos1.entity2);
                            break;
                        }
                        break;
                    case ExtVertXPos::NONE:
                    case ExtVertXPos::BAD_SIDE:
                    default:
                        _ASSERT(0);
                        break;
                    }
                } else if (hpos1.pos!=VertXPos::POS_CENTER && hpos2.pos!=VertXPos::POS_CENTER
                           && xsize) {
                    //Ok, now one of them (hpos1) is left, the other is right (hpos2)
                    //We can insert distances only if none of them is a-b-type AND if the user specified some size
                    double off2 = hpos2.pos == VertXPos::POS_LEFT_BY ? -gap1-act_size2-gap2 :
                                  hpos2.pos == VertXPos::POS_RIGHT_BY ? +gap1+act_size2+gap2  :
                                  hpos2.pos == VertXPos::POS_LEFT_SIDE ? -gap1-act_size2 :
                                  hpos2.pos == VertXPos::POS_RIGHT_SIDE ? +gap1-act_size2 : 0;
                    off2 += hpos2.offset;
                    vdist.Insert(index, hpos2.entity1->index, *xsize + off - off2, si, di);
                    vdist.InsertEntity(hpos1.entity1);
                    vdist.InsertEntity(hpos2.entity1);
                }
            }
        }
    } else /*IsByRefSymbol()==true*/ {
        //We do not add a new element to 'vdist' for byref symbols.
        //Instead, we update the space requirements of the last entity (which is the element referenced by this symbol)
        const auto si = vdist.GetIterator(pos_reftarget, true);
        const auto di = vdist.GetIterator(pos_reftarget, false);
        if (si != vdist.GetIteratorEnd() && di != vdist.GetIteratorEnd()) {
            const DistanceRequirementsSection elem = vdist.Get(si, di);
            switch (pos_type) {
            case msc::Symbol::ABOVE:
            case msc::Symbol::BELOW:
                //If we have a width, try applying that
                if (xsize.has_value())
                    if (const int l = elem.QueryLeftEntity(), r = elem.QueryRightEntity()
                            ;  l != DistanceRequirementsSection::NO_LEFT_ENTITY 
                            && r != DistanceRequirementsSection::NO_RIGHT_ENTITY) 
                        if (const double width = elem.Query(l, r)) {
                            double ref_width = width;
                            ref_width += elem.Query(l, DISTANCE_LEFT);
                            ref_width += elem.Query(r, DISTANCE_RIGHT);
                            if (ref_width < *xsize)
                                if (*style.read().makeroom)
                                    vdist.Insert(l, r, width + (*xsize - ref_width), si, di);
                        }
                break;
            case msc::Symbol::AROUND:
                if (const int l = elem.QueryLeftEntity(); l != DistanceRequirementsSection::NO_LEFT_ENTITY)
                    if (const double left_space = pos_distance + pos_adjust.x.from; 0 < left_space) {
                        const double existing_space = elem.Query(l, DISTANCE_LEFT);
                        if (*style.read().makeroom)
                            vdist.Insert(l, DISTANCE_LEFT, left_space + existing_space, si, di);
                    }
                if (const int r = elem.QueryRightEntity(); r != DistanceRequirementsSection::NO_RIGHT_ENTITY)
                    if (double right_space = pos_distance + pos_adjust.x.till; 0 < right_space) {
                        const double existing_space = elem.Query(r, DISTANCE_RIGHT);
                        if (*style.read().makeroom)
                            vdist.Insert(r, DISTANCE_RIGHT, right_space + existing_space, si, di);
                    }
                break;
            case msc::Symbol::RIGHTOF:
                if (xsize.has_value())
                    if (const int r = elem.QueryRightEntity(); r != DistanceRequirementsSection::NO_RIGHT_ENTITY)
                        if (double right_space = pos_distance + pos_adjust.x.till + *xsize; 0 < right_space) {
                            const double existing_space = elem.Query(r, DISTANCE_RIGHT);
                            if (*style.read().makeroom)
                                vdist.Insert(r, DISTANCE_RIGHT, right_space + existing_space, si, di);
                        }
                break;
            case msc::Symbol::LEFTOF:
                if (xsize.has_value())
                    if (const int l = elem.QueryLeftEntity(); l != DistanceRequirementsSection::NO_LEFT_ENTITY)
                        if (double left_space = pos_distance + pos_adjust.x.from + *xsize; 0 < left_space) {
                            const double existing_space = elem.Query(l, DISTANCE_LEFT);
                            if (*style.read().makeroom)
                                vdist.Insert(l, DISTANCE_LEFT, left_space + existing_space, si, di);
                        }
                break;
            case msc::Symbol::INVALID:
            default:
                break;
            }
        } else
            _ASSERT(0);
    }
    //Add a new element to vdist
    vdist.AddElementBottom(this);
    //Add activation status right away
    AddEntityLineWidths(vdist);
}


Range Symbol::CalculateOuterEdgeXFromXSizeOrHPos() const
{
    _ASSERT(!IsByRef());
    Range ret_x;
    double x1 = hpos1.CalculatePos(*chart, 0, gap1+act_size1, gap2);
    switch (hpos2.side) {
    case ExtVertXPos::NONE:
        switch (hpos1.side) {
        case ExtVertXPos::LEFT:
            ret_x.from = x1;
            ret_x.till = x1 + *xsize;
            break;
        case ExtVertXPos::RIGHT:
            ret_x.from = x1 - *xsize;
            ret_x.till = x1;
            break;
        case ExtVertXPos::CENTER:
            ret_x.from = x1 - *xsize/2;
            ret_x.till = x1 + *xsize/2;
            break;
        default:
            _ASSERT(0);
            break;
        }
        break;
    case ExtVertXPos::RIGHT:
        ret_x.till = hpos2.CalculatePos(*chart, 0, gap1+act_size2, gap2);
        if (hpos1.side == ExtVertXPos::LEFT)
            ret_x.from = x1;
        else //can only be center
            ret_x.from = 2*x1 - ret_x.till;
        break;
    case ExtVertXPos::CENTER:
        //here hpos1 can only be LEFT
        ret_x.from = x1;
        ret_x.till = 2*hpos2.CalculatePos(*chart, 0, gap1+act_size2, gap2) - x1;
        break;
    default:
        _ASSERT(0);
        break;
    }
    ret_x.Expand(style.read().line.LineWidth()/2);
    return ret_x;
}

//This is called when we finally know outer_edge.x and can calculate
//Y size with a certain aspect ratio. 
void Symbol::SetYSize() {
    if (!ysize) {
        if (symbol_type == SHAPE) {
            const XY s = chart->Shapes[*style.read().shape].GetMax().Spans();
            ysize = s.y * outer_edge.x.Spans() / s.x;
        } else if (symbol_type == INLINE) {
            if (!inlining_data.chart) {
                //Chart not valid, we do not draw anything
                valid = false;
                outer_edge.y.MakeInvalid();
                height = 0;
                return;
            }
            inlining_data.chart->GetTotal().x.Spans();
            ysize = inlining_data.chart->GetTotal().y.Spans() * (outer_edge.x.Spans() / inlining_data.chart->GetTotal().x.Spans());
        } else
            _ASSERT(0); //all other ysize has been set in PostParseProcess() (or RECTANGLE in Width())
    }
    const double lw = style.read().line.LineWidth();
    outer_edge.y.from = chart->arcVGapAbove;
    outer_edge.y.till = lw + *ysize + chart->arcVGapAbove;
}


void Symbol::Layout(Canvas &canvas, AreaList *cover)
{
    _ASSERT(!IsByRef());
    //if no xsize has been specified and only one x pos (so we cannot calculate x size from x positions)
    if (!xsize && hpos2.side == ExtVertXPos::NONE) {
        //defer x size calculation to when we know about the y size
        _ASSERT(symbol_type == SHAPE); //all other ysize has been set in PostParseProcess() (or RECTANGLE in Width())
        outer_edge.MakeInvalid();
        //Set height to zero not to disturb layouting and automatic pagination
        height = 0;
        return; // We can only determine x size (from specified y size) after Layout()
    }
    //Calculate x positions
    outer_edge.x = CalculateOuterEdgeXFromXSizeOrHPos();
    //if no markers were specified, we draw it here and assign room
    //else we are done here
    if (vpos.src.length() || vpos.dst.length()) {
        outer_edge.y.MakeInvalid();
        //Set height to zero not to disturb layouting and automatic pagination
        height = 0;
        return; //No call to CommentHeight(), it will be done in PostPosProcess()?
    }
    SetYSize();
    CalculateAreaFromOuterEdge(canvas);
    area_important = area;
    chart->NoteBlockers.Append(this);
    const double shadow_offset = style.read().shadow.offset.value_or(0);
    if (cover) {
        if (shadow_offset)
            *cover = area + area.CreateShifted(XY(shadow_offset, shadow_offset));
        else
            *cover = area;
    }
    height = outer_edge.y.till + shadow_offset + chart->arcVGapBelow;
    LayoutComments(canvas, cover);
}

bool Symbol::LayoutByRef(Canvas& canvas, const AreaList &target_cover, AreaList* cover) {
    _ASSERT(IsByRef());
    const Block& bb = target_cover.GetBoundingBox();
    if (bb.IsInvalid() || bb.x.Spans() < 1 || bb.y.Spans() < 1) {
        chart->Error.Error(file_pos.start, "The reference element for this symbol does not appear. Cannot show symbol either.");
        chart->Error.Error(pos_reftarget->file_pos.start, file_pos.start, "This is the referenced element.");
        valid = false;
        outer_edge.MakeInvalid();
        return false;
    }

    const double lw = style.read().line.LineWidth();
    if (xsize && *xsize<0) xsize = bb.x.Spans()-lw;
    if (ysize && *ysize<0) ysize = bb.y.Spans()-lw;

    outer_edge.x = { 0 , *xsize + lw };
    SetYSize();
    _ASSERT(outer_edge.UpperLeft() == XY(0, 0));
    switch (pos_type) {
    case ABOVE:
    case BELOW:
        switch (pos_indent.value_or(ESymbolIndent::CENTER)) {
        case ESymbolIndent::LEFT:
            outer_edge.x.Shift(bb.x.from + pos_offset);
            break;
        case ESymbolIndent::CENTER:
            outer_edge.x.Shift(bb.x.MidPoint() - outer_edge.x.MidPoint() + pos_offset);
            break;
        case ESymbolIndent::RIGHT:
            outer_edge.x.Shift(bb.x.till - outer_edge.x.till + pos_offset);
            break;
        default:
            break;
        }
        break;
    case RIGHTOF:
    case LEFTOF:
        switch (pos_indent.value_or(ESymbolIndent::CENTER)) {
        case ESymbolIndent::TOP:
            outer_edge.y.Shift(bb.y.from + pos_offset);
            break;
        case ESymbolIndent::CENTER:
            outer_edge.y.Shift(bb.y.MidPoint() - outer_edge.y.MidPoint() + pos_offset);
            break;
        case ESymbolIndent::BOTTOM:
            outer_edge.y.Shift(bb.y.till - outer_edge.y.till + pos_offset);
            break;
        case ESymbolIndent::ABOVE_CENTERLINE:
            outer_edge.y.Shift(pos_reftarget->GetCenterline(pos_type==RIGHTOF ? CenterlineSelector::RIGHT_SIDE : CenterlineSelector::LEFT_SIDE) - outer_edge.y.till + pos_offset);
            break;
        case ESymbolIndent::CENTERLINE:
            outer_edge.y.Shift(pos_reftarget->GetCenterline(pos_type == RIGHTOF ? CenterlineSelector::RIGHT_SIDE : CenterlineSelector::LEFT_SIDE) - outer_edge.y.MidPoint() + pos_offset);
            break;
        case ESymbolIndent::BELOW_CENTERLINE:
            outer_edge.y.Shift(pos_reftarget->GetCenterline(pos_type == RIGHTOF ? CenterlineSelector::RIGHT_SIDE : CenterlineSelector::LEFT_SIDE) + pos_offset);
            break;
        default:
            break;
        }
        break;
    case AROUND:
        outer_edge = bb.CreateExpand(pos_distance);
        break;
    default:
    case INVALID:
        _ASSERT(0);
        break;
    }
    switch (pos_type) {
    case ABOVE:
        outer_edge.y.Shift(bb.y.from - outer_edge.y.till - pos_distance);
        break;
    case BELOW:
        outer_edge.y.Shift(bb.y.till + pos_distance);
        break;
    case RIGHTOF:
        outer_edge.x.Shift(bb.x.till + pos_distance);
        break;
    case LEFTOF:
        outer_edge.x.Shift(bb.x.from - outer_edge.x.till - pos_distance);
        break;
    case INVALID:
    case AROUND:
    default: break;
    }
    const double xdelta = pos_adjust.x.from + pos_adjust.x.till;
    if (0 < xdelta && outer_edge.x.Spans() + xdelta <= 1) {
        chart->Error.Error(file_pos.start, "This symbol is of zero width even when its size is adjusted. Ignoring it.");
        height = 0;
        outer_edge = { 0,0,0,0 };
        return false;

    } else if (xdelta < 0 && outer_edge.x.Spans() + xdelta <= 0) {
        chart->Error.Error(pos_adjust_loc, "Using this adjustment reduces the width of the symbol to zero. Ignoring it.");
        height = 0;
        outer_edge = { 0,0,0,0 };
        return false;
    }
    const double ydelta = pos_adjust.y.from + pos_adjust.y.till;
    if (0 < ydelta && outer_edge.y.Spans() + ydelta <= 1) {
        chart->Error.Error(file_pos.start, "This symbol is of zero height even when its size is adjusted. Ignoring it.");
        height = 0;
        outer_edge = { 0,0,0,0 };
        return false;

    } else if (ydelta < 0 && outer_edge.y.Spans() + ydelta <= 0) {
        chart->Error.Error(pos_adjust_loc, "Using this adjustment reduces the height of the symbol to zero. Ignoring it.");
        height = 0;
        outer_edge = { 0,0,0,0 };
        return false;
    }
    outer_edge.x.from -= pos_adjust.x.from;
    outer_edge.x.till += pos_adjust.x.till;
    outer_edge.y.from -= pos_adjust.y.from;
    outer_edge.y.till += pos_adjust.y.till;
    //TODO: if compress is 'yes', try moving the outer_edge closer to the target
    CalculateAreaFromOuterEdge(canvas);
    if (pos_type == AROUND && (symbol_type == RECTANGLE || symbol_type == ARC)
            && !style.read().fill.IsFullyOpaque()) {
        //Make the area_draw a wire frame
        area_draw = area.CreateExpand(chart->trackFrameWidth) - area;
        //leave area_important empty.
        draw_is_different = true;
        area_draw_is_frame = true;
    } else
        area_important = area;
    chart->NoteBlockers.Append(this);
    const double shadow_offset = style.read().shadow.offset.value_or(0);
    if (cover) {
        if (shadow_offset)
            *cover = area + area.CreateShifted(XY(shadow_offset, shadow_offset));
        else
            *cover = area;
    }
    height = outer_edge.y.till + shadow_offset + chart->arcVGapBelow;
    LayoutComments(canvas, cover);
    return *style.read().makeroom;
}


void Symbol::ShiftBy(double y)
{
    LabelledArc::ShiftBy(y);
    if (outer_edge.y.IsInvalid()) return;
    outer_edge.y.Shift(y);
}

void Symbol::PlaceWithMarkers(Canvas &canvas)
{
    if (!outer_edge.y.IsInvalid()) return; //Already laid out: a byref symbol
    //We used markers, calculate "area" and "outer_edge.y" now
    if (vpos.src.length())
        outer_edge.y.from = chart->GetMarker(vpos.src)->y;
    if (vpos.dst.length())
        outer_edge.y.till = chart->GetMarker(vpos.dst)->y;

    if (vpos.src.length()==0)
        outer_edge.y.from = outer_edge.y.till - *ysize;
    if (vpos.dst.length()==0)
        outer_edge.y.till = outer_edge.y.from + *ysize;

    if (outer_edge.y.from > outer_edge.y.till)
        std::swap(outer_edge.y.from, outer_edge.y.till);
    else if (outer_edge.y.from == outer_edge.y.till)
        outer_edge.y.Expand(*ysize/2);

    outer_edge.y.Expand(style.read().line.LineWidth()/2);

    if (outer_edge.x.IsInvalid()) {
        _ASSERT(symbol_type == SHAPE); //all other xsize has been set in Layout()
        const XY s = chart->Shapes[*style.read().shape].GetMax().Spans();
        xsize = s.x*outer_edge.y.Spans()/s.y;
        outer_edge.x = CalculateOuterEdgeXFromXSizeOrHPos();
    }

    CalculateAreaFromOuterEdge(canvas);
    //set yPos and height so that GetYExtent returns the right range
    //to determine if this element needs to be drawn on a particular page
    yPos = outer_edge.y.from;
    height = outer_edge.y.Spans();
}

/** Calculates the `area` field from the `outer_edge` field.
 * It also reflows label if needed.*/
void Symbol::CalculateAreaFromOuterEdge(Canvas &canvas)
{
    switch (symbol_type) {
        case ARC:
            area = Contour(outer_edge.Centroid(), outer_edge.x.Spans()/2,
                           outer_edge.y.Spans()/2);
            break;
        case CROSS:
            area = outer_edge.CreateExpand(-style.read().line.LineWidth()/2);
            break;
        case SHAPE:
            area = chart->Shapes.Cover(*style.read().shape, outer_edge);
            break;
        case INLINE:
        case RECTANGLE:
            area = style.read().line.CreateRectangle_OuterEdge(outer_edge.CreateExpand(-style.read().line.LineWidth()/2));
            break;
        case ELLIPSIS:
            const double rx = *xsize / 2;
            const double ry = *ysize / (6 + 4 * ellipsis_space_ratio);
            XY c(outer_edge.x.MidPoint(), outer_edge.y.from + style.read().line.LineWidth()/2 + chart->arcVGapAbove + ry);
            area = Contour(c, rx, ry);
            area += Contour(c + XY(0, (2+2*ellipsis_space_ratio)*ry), rx, ry);
            area += Contour(c + XY(0, (4+4*ellipsis_space_ratio)*ry), rx, ry);
            break;
    }
    const double space = std::max(0., outer_edge.x.Spans() - 2*style.read().line.LineWidth());
    if (parsed_label.IsWordWrap() && parsed_label.size()>0) {
        /*const double overflow = */parsed_label.Reflow(canvas, chart->Shapes, space);
        //OverflowWarning(overflow, "", i, src);
    } else {
        CountOverflow(space);
    }
    const XY twh = parsed_label.getTextWidthHeight();
    if (twh.x>0) {
        const Block b(outer_edge.Centroid()-twh/2, outer_edge.Centroid()+twh/2);
        area += parsed_label.Cover(chart->Shapes, b.x.from, b.x.till, b.y.from);
    }
    area.arc = this;
    area.mainline = Block(chart->GetDrawing().x, area.GetBoundingBox().y);
}

void Symbol::PostPosProcess(Canvas &canvas, Chart *ch)
{
    if (!valid) return;
    if (outer_edge.x.Spans()<=0 || outer_edge.y.Spans()<=0) return;
    if (inlining_data.chart) {
        const Block mid = outer_edge.CreateExpand(-style.read().line.LineWidth() / 2);
        inlining_data.parent_to_inline
            = TRMatrix::CreateShift(inlining_data.chart->GetTotal().UpperLeft())
            * TRMatrix::CreateScale(XY{ inlining_data.chart->GetTotal().x.Spans() / mid.x.Spans(), inlining_data.chart->GetTotal().y.Spans() / mid.y.Spans() })
            * TRMatrix::CreateShift(-mid.UpperLeft());
        area_draw = area.CreateExpand(chart->trackFrameWidth) - area;
        draw_is_different = true;
        area_draw_is_frame = true;
    }
    //Expand area and add us to chart's all covers list
    LabelledArc::PostPosProcess(canvas, ch);
    const XY twh = parsed_label.getTextWidthHeight();
    const Block b(outer_edge.Centroid()-twh/2, outer_edge.Centroid()+twh/2);
    chart->HideEntityLines(parsed_label.Cover(chart->Shapes, b.x.from, b.x.till, b.y.from));              
}

void Symbol::RegisterLabels()
{
    const XY twh = parsed_label.getTextWidthHeight();
    const Block b(outer_edge.Centroid()-twh/2, outer_edge.Centroid()+twh/2);
    //TODO: Fix labels for shape
    chart->RegisterLabel(parsed_label, LabelInfo::SYMBOL,
        b.x.from, b.x.till, b.y.from);
}

void Symbol::CollectIsMapElements(Canvas &canvas)
{
    const XY twh = parsed_label.getTextWidthHeight();
    const Block b(outer_edge.Centroid()-twh/2, outer_edge.Centroid()+twh/2);
    //TODO: Fix labels for shape
    parsed_label.CollectIsMapElements(chart->ismapData, canvas, chart->Shapes,
        b.x.from, b.x.till, b.y.from);
}


void Symbol::Draw(Canvas &canvas, EMscDrawPass pass) const
{
    if (!valid) return;
    if (outer_edge.x.Spans() <= 0 || outer_edge.y.Spans() <= 0) return;
    if (pass!=draw_pass) return;
    if (canvas.does_graphics() && canvas.cannot_draw()) return;
    bool draw_label = parsed_label.size()>0;
    switch (symbol_type) {
        case ARC:
        case ELLIPSIS: {
            Contour a;
            if (symbol_type == ARC)
                a = Contour(outer_edge.Centroid(), outer_edge.x.Spans()/2,
                    outer_edge.y.Spans()/2);
            else {
                const double rx = *xsize / 2;
                const double ry = *ysize / (6 + 4 * ellipsis_space_ratio);
                XY c(outer_edge.x.MidPoint(), outer_edge.y.from + style.read().line.LineWidth() / 2 + chart->arcVGapAbove + ry);
                a = Contour(c, rx, ry);
                a += Contour(c + XY(0, (2 + 2 * ellipsis_space_ratio) * ry), rx, ry);
                a += Contour(c + XY(0, (4 + 4 * ellipsis_space_ratio) * ry), rx, ry);
            }
            if (canvas.does_graphics()) {
                canvas.Shadow(a, style.read().shadow);
                canvas.Fill(a.CreateExpand(-style.read().line.LineWidth()/2-style.read().line.Spacing()),
                            style.read().fill);
                canvas.Line(a.CreateExpand(-style.read().line.LineWidth()/2), style.read().line);
            } else {
                const Block bb = a.GetBoundingBox();
                canvas.Add(GSShape(std::move(a), style.read().line, style.read().fill, style.read().shadow, bb, parsed_label));
                draw_label = false;
            }
            break;
        }
        case SHAPE:
            if (canvas.does_graphics()) {
                chart->Shapes.Draw(canvas, *style.read().shape, outer_edge, style.read().line, style.read().fill);
                if (parsed_label.size()) {
                    const XY twh = parsed_label.getTextWidthHeight();
                    const Block b = chart->Shapes[*style.read().shape].GetLabelPos(outer_edge);
                    if (!b.IsInvalid()) {
                        const double r = std::min(1., std::min(b.x.Spans()/twh.x, b.y.Spans()/twh.y));
                        cairo_save(canvas.GetContext());
                        cairo_scale(canvas.GetContext(), r, r);
                        const double yoff = (b.y.Spans() - twh.y*r)/2;
                        parsed_label.Draw(canvas, chart->Shapes, b.x.from/r, b.x.till/r,
                                          (b.y.from + yoff)/r);
                        cairo_restore(canvas.GetContext());
                    }
                }
            } else {
                canvas.AddMore(GSMscShape(chart->Shapes, *style.read().shape, outer_edge, style.read().line, style.read().fill, style.read().shadow, parsed_label, true));
                draw_label = false;
            }

            break;
        case CROSS:
            DrawLSym(canvas, outer_edge.Centroid(), outer_edge.Spans()/2, style.read().line);
            break;
        case RECTANGLE:
            if (canvas.does_graphics()) {
                //canvas operations on blocks take the midpoint
                const Block mid = outer_edge.CreateExpand(-style.read().line.LineWidth()/2);
                canvas.Shadow(mid, style.read().line, style.read().shadow);
                canvas.Fill(mid, style.read().line, style.read().fill);
                canvas.Line(mid, style.read().line);
            } else {
                canvas.Add(GSBox(outer_edge, style.read().line, style.read().fill, style.read().shadow, parsed_label, ETextAlign::Min));
                draw_label = false;
            }
            break;
        case INLINE: {            
            const Block mid = outer_edge.CreateExpand(-style.read().line.LineWidth() / 2);
            if (canvas.does_graphics()) {
                canvas.Shadow(mid, style.read().line, style.read().shadow);
                canvas.Fill(mid, style.read().line, style.read().fill);
            } else 
                canvas.Add(GSBox(outer_edge, LineAttr::None(), style.read().fill, style.read().shadow, {}, ETextAlign::Min));
            if (inlining_data.chart) {
                const bool err = canvas.Transform(inlining_data.parent_to_inline.Invert());
                if (err) 
                    chart->Error.Error(file_pos.start, "Cannot export this inlined chart as it is placed here with sheering or flip transformations and the output format cannot take them.");
                else {
                    inlining_data.chart->Draw(canvas, false, 0);
                    canvas.UnTransform();
                }
            }
            if (canvas.does_graphics())
                canvas.Line(mid, style.read().line);
            else {
                canvas.Add(GSBox(outer_edge, style.read().line, FillAttr::None(), ShadowAttr::None(), parsed_label, ETextAlign::Min));
                draw_label = false;
            }
            break;
        }
    }
    if (draw_label) {
        const XY twh = parsed_label.getTextWidthHeight();
        const Block b(outer_edge.Centroid()-twh/2, outer_edge.Centroid()+twh/2);
        parsed_label.Draw(canvas, chart->Shapes, b.x.from, b.x.till, b.y.from);
    }
}

void Symbol::InlinedChartGUIStateChange() const {
    if (inlining_data.chart) {
        chart->inline_gui_state[signature] = inlining_data.chart->SerializeGUIState();
        chart->GUIStateChanged();
    }
}


/////////////////////////////////////////////////////////////////////////////////////////////////////

/** Construct a floating note */
Note::Note(MscChart*msc, std::string_view pt, const FileLineColRange &ptm)
    : LabelledArc(EArcCategory::NOTE, msc),
    is_float(true), target(nullptr), point_toward(pt), point_toward_pos(ptm),
    float_dir_x(0), float_dir_y(0)
{
    draw_pass = EMscDrawPass::NOTE;
    SetStyleWithText("note", nullptr);
    if (chart->SkipContent()) return;
    //if we are replaying a procedure and could not find the pointer target among the entities, assume a marker
    if (point_toward.length() && chart->AllEntities.Find_by_Name(point_toward)==nullptr && chart->Reparsing()) {
        //decorate the marker name with the context....
        point_toward.append(chart->MyCurrentContext().file_pos.Print());
    }
}

/** Construct a comment, endnote or footnote */
Note::Note(MscChart*msc, ESide side)
    : LabelledArc(EArcCategory::COMMENT, msc),
    is_float(false), target(nullptr),
    float_dir_x(0), float_dir_y(0)
{
    draw_pass = EMscDrawPass::NOTE;
    SetStyleWithText(side==ESide::END ? "endnote" : "comment", nullptr);
}

Note::~Note()
{
    if (chart)
        chart->RemoveFromNotes(this);
}


void Note::SetStyleBeforeAttributes(AttributeList * l)
{
    //Fish out note pointer and see what kind of arrow attributes do we
    //accept
    if (l)
        for (auto i = l->begin(); i!=l->end(); /*nope*/) {
            if ((*i)->Is("note.pointer")) {
                AddAttribute(**i);
                l->erase(i++);
            } else {
                i++;
            }
        }
    if (style.read().note.pointer) {
        EArcArrowType t;
        switch (*style.read().note.pointer) {
        case NoteAttr::ARROW: t = EArcArrowType::ARROW; break;
        case NoteAttr::BLOCKARROW: t = EArcArrowType::BIGARROW; break;
        default: t = EArcArrowType::NONE;
        }
        if (style.read().arrow.read().GetArcArrowType()!=t) {
            MscArrowHeads arrow(t);
            arrow += style.read().arrow.read(); //this verifies that arrow types are applicable to 't'
            style.write().arrow.write() = arrow;
        }
        style.write().f_arrow = t;
    }
}

bool Note::AddAttribute(const Attribute &a)
{
    //We handle note.pos here
    //This allows us to set only either dir or distance component of the
    //position, relying on that of the style for the rest.
    //It also allows us to treat an attribute set on the command to mean
    //a stronger request than a setting on a style (If I am not mistaken
    //it is not done now, see Not::PlaceFloating()).
    if (is_float && a.Is("note.pos")) {
        if (!a.CheckType(EAttrType::STRING, chart->Error)) return true;
        //EAttrType::CLEAR is OK above with value = ""
        NoteAttr::EPosType tmp = NoteAttr::EPosType::POS_INVALID;
        if (a.value.length()==0) {
            //delete even the default orientation
            float_dir_x = float_dir_y = 0;
            float_dist.reset();
            style.write().note.def_float_dist = style.write().note.def_float_x =
                style.write().note.def_float_y = 0;
        } else if (Convert(a.value, tmp)) {
            switch(tmp) {
            default:
            case NoteAttr::POS_INVALID: _ASSERT(0); break;
            case NoteAttr::POS_NEAR: float_dist=-1; break;
            case NoteAttr::POS_FAR:  float_dist=+1; break;
            case NoteAttr::LEFT:     float_dir_x=-1; float_dir_y=0; break;
            case NoteAttr::RIGHT:    float_dir_x=+1; float_dir_y=0; break;
            case NoteAttr::UP:       float_dir_x=0; float_dir_y=-1; break;
            case NoteAttr::DOWN:     float_dir_x=0; float_dir_y=+1; break;
            case NoteAttr::LEFT_UP:    float_dir_x=-1; float_dir_y=-1; break;
            case NoteAttr::LEFT_DOWN:  float_dir_x=-1; float_dir_y=+1; break;
            case NoteAttr::RIGHT_UP:   float_dir_x=+1; float_dir_y=-1; break;
            case NoteAttr::RIGHT_DOWN: float_dir_x=+1; float_dir_y=+1; break;
            }
        } else
            a.InvalidValueError(CandidatesFor(tmp), chart->Error);
        return true;
    }
    if (a.Is("arrow.starttype") || a.Is("arrow.skiptype") || a.Is("arrow.midtype")) {
        chart->Error.Error(a, false, "Only the end type of a note poiner can be set. Ignoring '"+a.name+"' attribute.");
        return true;
    }
    return LabelledArc::AddAttribute(a);
}

void Note::AttributeNames(Csh &csh, bool is_float)
{
    LabelledArc::AttributeNames(csh);
    csh.AttributeNamesForStyle(is_float ? "note" : "comment");
}

bool Note::AttributeValues(std::string_view attr, Csh &csh, bool is_float)
{
    if (csh.AttributeValuesForStyle(attr, is_float ? "note" : "comment")) return true;
    return LabelledArc::AttributeValues(attr, csh);
}

ArcBase* Note::PostParseProcess(Canvas &canvas, bool hide, EntityRef &left, EntityRef &right,
                                Numbering& number, MscElement** note_target, ArcBase* byref_target)
{
    if (!valid) return nullptr;
    vspacing = 0; //really relevant only for endnotes...
    //ensure we have label
    if (label.length()==0) {
        chart->Error.Error(file_pos.start, is_float ? "Notes" : "Comments" +
                           string(" must contain a label. Ignoring note."),
            "Try adding a 'label' attribute or text after a colon (':').");
        valid = false;
        return nullptr;
    }

    //Now try to attach to the target, if not yet attached (as is the case for comments to entities)
    if (target == nullptr) {
        target = *note_target;
        if (target == nullptr) {
            valid = false;
            chart->Error.Error(file_pos.start, "This note has no prior element to note on. Ignoring it.",
                "Every note must follow a visible element which it makes a remark on.");
            return nullptr;
        }
    }

    //We do everything here even if we are hidden (numbering is not impacted by hide/show or collapse/expand)
    //Do not call LabelledArc::PostParseProcess, as we do not increase numbering for notes
    ArcBase *ret = ArcBase::PostParseProcess(canvas, hide, left, right, number, note_target, byref_target);
    //find or create detailed arrowhead structure (normally in ArcLabelled::PostParseProcess())
    _ASSERT(style.read().f_arrow==style.read().arrow.read().GetArcArrowType());
    if (style.read().f_arrow!=EArcArrowType::NONE)
        style.read().arrow.read().CreateArrowHeads();

    if (target == DELETE_NOTE || hide)
        return nullptr; //silently drop note - our target has disappeared, as well
    //OK, now attach the note
    if (is_float) {
        chart->Notes.Append(this);
    } else if (style.read().side != ESide::END) {
        //Do not attach endnotes. MscChart::PostParseProcessArcList will take them out
        //and will get appended to the end of the chart
        target->AttachComment(this);
    }
    return ret;
}

void Note::FinalizeLabels(Canvas &canvas)
{
    if (!valid) return;
    const LabelledArc *al = dynamic_cast<const LabelledArc*>(target);
    //skip numbering if target has no number
    if (*style.read().numbering &&
        (!al || !*al->GetStyle().read().numbering || !al->GetNumberText().length()))
        *style.write().numbering = false;
    if (*style.read().numbering) {
        numberingStyle = al->GetNumberingStyle();
        number_text = al->GetNumberText();
    }
    LabelledArc::FinalizeLabels(canvas);
}

//Side comments report zero width if they are word wrapped,
//but always set the 'had_X_comment' flag of 'distances'
void Note::Width(Canvas &canvas, DistanceRequirements &vdist)
{
    if (!valid) return;
    //Command::Width(canvas, distances); We may not have notes, do NOT call ancerstor
    if (is_float) {
        //reflow label if needed
        if (parsed_label.IsWordWrap()) {
            const double w = style.read().note.width.GetWidth(canvas, chart->Shapes, style.read().text);
            if (w>0) {
                const double overflow = parsed_label.Reflow(canvas, chart->Shapes, w);
                OverflowWarning(overflow, "Use the 'width' attribute to increase note width.", nullptr, nullptr);
            } else {
                chart->Error.Warning(file_pos.start, "Word wrapping is on, but no meaningful width is specified.", "Ignoring word wrapping. Try setting 'note.width'.");
            }
        }
        halfsize = parsed_label.getTextWidthHeight()/2 + XY(style.read().line.LineWidth(), style.read().line.LineWidth());
    } else {
        //Here we only make space if the note is on the side
        const double w = parsed_label.getSpaceRequired();
        switch (*style.read().side) {
        case ESide::LEFT:
            vdist.Insert(chart->LNote->index, DISTANCE_LEFT, w);
            vdist.had_l_comment = true;
            break;
        default:
            _ASSERT(0);
            FALLTHROUGH;
        case ESide::RIGHT:
            vdist.Insert(chart->RNote->index, DISTANCE_RIGHT, w);
            vdist.had_r_comment = true;
            break;
        case ESide::END:
            vdist.Insert(chart->LSide->index, chart->LSide->index, w);
            break;
        }
    }
}

void Note::Layout(Canvas &canvas, AreaList * cover)
{
    if (!valid) return;
    if (!is_float)  {
        //Only comments added here. Notes will be added after their placement
        chart->NoteBlockers.Append(this);
        if (style.read().side == ESide::END) {
            //Endnotes are laid out normally here
            //Start with reflowing the label if needed
            const double space = chart->XCoord(chart->EndEntity->pos) - 2*chart->sideNoteGap;
            if (parsed_label.IsWordWrap()) {
                const double overflow = parsed_label.Reflow(canvas, chart->Shapes, space);
                OverflowWarning(overflow, "", nullptr, nullptr);
            } else {
                CountOverflow(space);
            }
            yPos = 0;
            area = parsed_label.Cover(chart->Shapes, chart->sideNoteGap,
                          chart->XCoord(chart->EndEntity->pos) - chart->sideNoteGap,
                          yPos);
            area.arc = this;
            height = area.GetBoundingBox().y.till + chart->arcVGapBelow;
            area.mainline = Block(chart->GetTotal().x, area.GetBoundingBox().y);
            if (cover)
                *cover += GetCover4Compress(area);
            return;
        }
    }
    height = 0;
}

Contour Note::CoverBody(const XY &center) const//places upper left corner to 0,0
{
    _ASSERT(is_float);
    return style.read().line.CreateRectangle_Midline(center-halfsize, center+halfsize);
}

const double pointer_width_min=10; ///<The minimum width of a callout pointer at its base
const double pointer_width_max=50; ///<The maximum width of a callout pointer at its base
const double pointer_width_div=50; ///<The ratio of the length of a callout pointer and its width (before applying min/max)

double Note::pointer_width(double distance) const
{
    _ASSERT(is_float);
    switch (*style.read().note.pointer) {
    default: _ASSERT(0); FALLTHROUGH;
    case NoteAttr::NONE:
        return 0;
    case NoteAttr::CALLOUT:
        return std::min(pointer_width_max, pointer_width_min + distance/pointer_width_div);
    case NoteAttr::BLOCKARROW:
        return 20; //This is an arbitrary number determining the width of the arrow body - maybe have this as an attr?
    case NoteAttr::ARROW:
        return style.read().line.LineWidth();
    }
}

Contour Note::cover_pointer(const XY &pointto, const XY &center) const //places upper left corner of the body to 0,0
{
    _ASSERT(is_float);
    const double l = center.Distance(pointto);
    if (contour::test_zero(l)) return Contour();
    const double width = pointer_width(l);
    //_ASSERT(style.read().note.IsComplete());  //width may be unset.
    switch (*style.read().note.pointer) {
    default: _ASSERT(0); FALLTHROUGH;
    case NoteAttr::NONE: return Contour();
    case NoteAttr::CALLOUT: {
        const XY a = (center-pointto).Rotate90CCW()/l*width/2;
        XY p1 = center-a;
        XY p2 = center+a;
        //center+-a may lie outside the body
        //Ensure that we return sections that extrude from 'body' only
        //towards 'pointto'
        Contour body = CoverBody(center);
        if (body.IsWithin(p1) == contour::EPointRelationType::WI_OUTSIDE) {
            const Edge e(p1, pointto);
            const Range r = body.Cut(e);
            if (!r.IsInvalid())
                p1 = e.Pos2Point(r.till);
        }
        if (body.IsWithin(p2) == contour::EPointRelationType::WI_OUTSIDE) {
            const Edge e(p2, pointto);
            const Range r = body.Cut(e);
            if (!r.IsInvalid())
                p2 = e.Pos2Point(r.till);
        }
        return Contour(pointto, p1, p2);
    }
    case NoteAttr::BLOCKARROW:
    case NoteAttr::ARROW:
        break;
    }
    //Do an arrow between pointto - XY(l,0) and pointto
    std::vector<double> v(2), a(2);
    v[0] = pointto.x - l; v[1] = pointto.x;
    a[0] = a[1] = 0;
    Contour ret;
    if (*style.read().note.pointer == NoteAttr::BLOCKARROW) {
        const double size_mul = 3;
        std::vector<Contour> vc;
        v[0] *= size_mul;
        v[1] *= size_mul;
        ret = style.read().arrow.read().
            BigContour(v, a, (pointto.y-width/3)*size_mul, (pointto.y+width/3)*size_mul,
                       true, false, &style.read().line, nullptr, vc);
        ret.Scale(1/size_mul);
    } else {
        const Contour clip = style.read().arrow.read().
            ClipForLine(pointto, 0, true, false, EArrowEnd::END,
                        style.read().line, style.read().line);
        ret = style.read().arrow.read().
            Cover(pointto, 0, true, false, EArrowEnd::END, style.read().line, style.read().line);
        ret += Contour(v[0], v[1], pointto.y-width/2, pointto.y+width/2) * (chart->GetTotal()-clip);
    }
    //Now rotate around pointto so that startpoint is in "center"
    const double deg =rad2deg(atan2(-(center-pointto).y, -(center-pointto).x));
    ret.RotateAround(pointto, deg);
    return ret;
}

/** Creates an outer boundary box for a region, which is a triangle.
 * @param outer The bounding box of the target expanded according to the region (distance).
 * @param center The centroid of the target
 * @param dir_x The horizontal position of the region (left:-1, middle:0, right: +1)
 * @param dir_y The vertical position of the region (up:-1, middle:0, down: +1)
 * @returns The contour of the region.
 * */
//dir_x is +2 at the right edge of the box searching, +1 in the right third
//+ in the middle third, -1 in the left third and -2 along the left edge.
//We return the region to check and also a starting point
Contour Note::GetRegionMask(const Block &outer, const XY &center,
                                   int dir_x, int dir_y)
{
    XY A, B;
    const XY third(outer.x.Spans()/3, outer.y.Spans()/3);
    switch (dir_x) {
    default:
        _ASSERT(0);
        FALLTHROUGH;
    case -2: A.x = B.x = outer.x.from; break;
    case -1: A.x = outer.x.from; B.x = A.x + third.x; break;
    case  0: A.x = outer.x.from + third.x; B.x = A.x + third.x; break;
    case +1: B.x = outer.x.till; A.x = B.x - third.x; break;
    case +2: B.x = A.x = outer.x.till;
    }
    switch (dir_y) {
    default:
        _ASSERT(0);
        FALLTHROUGH;
    case -2: A.y = B.y = outer.y.from; break;
    case -1: A.y = outer.y.from; B.y = A.y + third.y; break;
    case  0: A.y = outer.y.from + third.y; B.y = A.y + third.y; break;
    case +1: B.y = outer.y.till; A.y = B.y - third.y; break;
    case +2: B.y = A.y = outer.y.till;
    }

    return Contour(A, B, center);
}

/** If the user proscribed an 'at' clause, get the points on the
 * contour of the target, which can be pointer targets.
 * These points are essentially where the entity line of the entity or
 * the horizontal line of the marker crosses the contour of the target.
 * We also return a forward tangent point of the contour at the target 
 * point for both target points. 
 * Return empty if the user specified no `at` clause or if there
 * are no valid crossings - thus any point on the contour can do.
 * This generates error messages, if there are no valid entity/marker
 * of the given name or there is no valid crossing. */
std::optional<std::array<std::pair<XY, XY>, 2>> Note::GetPointerTarget() const
{
    _ASSERT(is_float);
    std::optional<std::array<std::pair<XY, XY>, 2>> ret;
    if (point_toward.length()==0) return ret;
    auto ei = chart->AllEntities.Find_by_Name(point_toward);
    auto m = chart->GetMarker(point_toward);
    if (ei == nullptr && m == nullptr) {
        chart->Error.Error(point_toward_pos.start, "'" + StringFormat::ConvertToPlainTextCopy(point_toward) + "' is neither an entity nor a marker. Ignoring it.");
        return ret;
    }
    DoubleMap<bool> section(false);
    if (ei) {
        if (m) {
            chart->Error.Warning(point_toward_pos.start, "You have specified both an entity and a marker with the name '" + point_toward + "'. I use the entity here.");
            chart->Error.Warning(ei->file_pos, point_toward_pos.start, "Place of the entity definition.");
            chart->Error.Warning(m->line, point_toward_pos.start, "Place of the marker definition.");
        }
        ret.emplace();
        const double x = chart->XCoord(ei);
        const Range y = target->GetAreaToNote().GetBoundingBox().y;
        const Edge e(XY(x, y.from-1), XY(x, y.till+1));
        Range r = target->GetAreaToNote().CutWithTangent(e, ret->at(0), ret->at(1));
        if (r.IsInvalid()) {
            r = target->GetAreaToNote2().CutWithTangent(e, ret->at(0), ret->at(1));
            if (r.IsInvalid()) {
                //OK, the entity line does not cut the area of the target. This may be because of activation
                //or a very thick entity line. At this point we do not yet have ei->status filled in.
                //see which topl-level contour of the target area is closest to
                double d = DBL_MAX;
                double x_new = x;
                const Contour *area = nullptr;
                const Contour *a = &target->GetAreaToNote();
                for (unsigned u = 0; u<a->size(); u++) {
                    const auto &o = (*a)[u].GetBoundingBox().x;
                    if (o.Distance(x) < d) {
                        d = o.Distance(x);
                        x_new = x < o.from ? o.from : o.till;
                        area = a;
                    }
                }
                a = &target->GetAreaToNote2();
                for (unsigned u = 0; u<a->size(); u++) {
                    const auto &o = (*a)[u].GetBoundingBox().x;
                    if (o.Distance(x) < d) {
                        d = o.Distance(x);
                        x_new = x < o.from ? o.from : o.till;
                        area = a;
                    }
                }
                //if we are +-10 pixels within, allow it
                if (area && d<10) {
                    r = area->CutWithTangent(Edge(XY(x_new, y.from-1), XY(x_new, y.till+1)), ret->at(0), ret->at(1));
                    if (!r.IsInvalid())
                        return ret;
                }

                //we return an error
                chart->Error.Error(point_toward_pos.start, "Entity '" + point_toward + "' is not above the target of the note. Ignoring the 'at' clause.");
                chart->Error.Error(target->file_pos.start, point_toward_pos.start, "This is the target of the note.");
                ret.reset();
            }
        }
        return ret;
    }
    //now we must have a valid marker in "mi"
    ret.emplace();
    const double y = m->y;
    const Range x = target->GetAreaToNote().GetBoundingBox().x;
    const Edge e(XY(x.from-1, y), XY(x.till+1, y));
    const Range r = target->GetAreaToNote().CutWithTangent(e, ret->at(0), ret->at(1));
    if (r.IsInvalid()) {
        const Range r2 = target->GetAreaToNote2().CutWithTangent(e, ret->at(0), ret->at(1));
        if (r2.IsInvalid()) {
            chart->Error.Error(point_toward_pos.start, "Marker '" + StringFormat::ConvertToPlainTextCopy(point_toward) + "' is not level with the target of the note. Ignoring the 'at' clause.");
            chart->Error.Error(target->file_pos.start, point_toward_pos.start, "This is the target of the note.");
            chart->Error.Error(m->line, point_toward_pos.start, "Place of the marker definition.");
            ret.reset();
        }
    }
    return ret;
}

/**This struct holds note placement score*/
struct msc::score_t {
    double a, b, c;
    constexpr score_t() noexcept = default;
    constexpr score_t(double aa, double bb = 0, double cc = 0) noexcept : a(aa), b(bb), c(cc) {}
    constexpr score_t operator +(const score_t& o) noexcept { return {a+o.a, b+o.b, c+o.c}; }
    constexpr score_t& operator +=(const score_t& o) noexcept { a += o.a; b += o.b; c += o.c; return *this; }
    constexpr score_t operator *(double m) const noexcept {return score_t(a*m, b*m, c*m);}
    constexpr auto operator<=>(const score_t &o) const noexcept = default;
    std::string Dump() const { char buff[512]; snprintf(buff, sizeof(buff), "%g:%g:%g", a, b, c); return buff; }
};

//These are added to the region_blocks[].score.a or .b, can be any value
constexpr double dist_award_match = 0;                   ///<Score awarded at floating note placement for regions with the same distance specified by the user
constexpr double dist_penalty_mul = -10;                 ///<Score penalty at floating note placement for regions with a different distance specified by the user (multiplied by the difference in distance)
constexpr double corner_award_match = +15;               ///<Score awarded at floating note placement for regions at the corner specified by the user (if the user specified a corner)
constexpr double corner_award_neighbour_side = +5;       ///<Score awarded at floating note placement for regions at the side of the corner specified by the user (if the user specified a corner)
constexpr double corner_penalty_neighbour_corner = -15;  ///<Score penalty at floating note placement for regions at corner neighbouring the corner specified by the user (if the user specified a corner)
constexpr double corner_penalty_other_side = -20;        ///<Score penalty at floating note placement for regions at sides not neighbouring (further) the corner specified by the user (if the user specified a corner)
constexpr double corner_penalty_opposite = -30;          ///<Score penalty at floating note placement for the corner opposite to the corner specified by the user (if the user specified a corner)
constexpr double side_award_match = +15;                 ///<Score awarded at floating note placement for regions at the side specified by the user (if the user specified a side)
constexpr double side_award_neighbour = +10;             ///<Score awarded at floating note placement for regions at a corner neighbouring the side specified by the user (if the user specified a side)
constexpr double side_penalty_neighbour_side = -5;       ///<Score penalty at floating note placement for regions at a side neighbouring to the side specified by the user (if the user specified a side)
constexpr double side_penalty_other_corner = -20;        ///<Score penalty at floating note placement for regions at a corner not neighbouring (further) the side specified by the user (if the user specified a side)
constexpr double side_penalty_opposite = -30;            ///<Score penalty at floating note placement for regions at a side opposite to the side specified by the user (if the user specified a side)
//the below penalties are calculated after placement, must be negative
//so that we can detect when the score cannot improve any better and bail out
constexpr score_t penalty_for_outside_drawing(0, -10);   ///<Score penalty at floating note placement for regions outside the drawing area of the chart
constexpr score_t penalty_for_outside_total(0, -10);     ///<Score penalty at floating note placement for regions outside the total area of the chart (in addition to penalty_for_outside_drawing)
constexpr score_t penalty_for_covered_region_mul(0, -20);///<Score penalty at floating note placement for regions covered. Multiplied by a [0..1] value (% of region free)
constexpr score_t penalty_body_cover_something(0, -10);  ///<Score penalty at floating note placement for placements that cover something (applied in addition to penalty_body_cover_something_mul)
constexpr score_t penalty_body_cover_something_mul(0, -10);///<Additional score penalty at floating note placement for placements that cover something (applied in addition to penalty_body_cover_something) Multiplies a [0..1] value (percent of the comment covering)
constexpr score_t penalty_body_cover_imp(0, -50);        ///<Score penalty at floating note placement for placements that cover something important (applied in addition to penalty_body_cover_imp_mul)
constexpr score_t penalty_body_cover_imp_mul(0, -50);    ///<Additional score penalty at floating note placement for placements that cover something important (applied in addition to penalty_body_cover_imp) Multiplies a [0..1] value (percent of the comment covering)
constexpr score_t penalty_for_pointer_angle_mul(0, -0.07); ///<Penalty if the angle of the note pointer diverges from the angle suitable for this region. Multiplies a [0..90] value (degree)
constexpr score_t penalty_for_pointer_vs_target_angle_mul(0, -0.3); ///<Penalty if the angle of the note pointer diverges from perpendicular to the edge of the pointed contour. Multiplies a [0..60] value (angle - 30 degree), no penalty below 30 degrees.
constexpr double worst_value = -MaxVal(worst_value);     ///<The worst possible score at floating note placement
constexpr score_t worst_point(worst_value, worst_value, worst_value); ///<The worst possible point a placement can get at floating note placement
constexpr score_t neutral_point(0,0,0);                  ///<The neutral scoring - dont change the points we awarded so far.

/** Score a region during floating node placement. */
double ScoreRegion(const OptAttr<int> &wanted_dist,
                   int wanted_x, int wanted_y,
                   int dist, int x, int y)
{

    //if distance is specified and matches, we award +10 points,
    //    if does not match we penalize diff*5 points
    //if two directions specified, we award 15 points to the corner
    //      and 5 points to the neighbouring sides
    //      -15 points to neighbouring corners, -20 to other sides and -30 to opposite corner
    //       CPNC   CAN  CAM
    //        +--  ----  ==+  <-if this corner is selected
    //    CPNC|            || CAM = Corner_Award_Match (see consts above)
    //
    //    CPOS|            | CAN
    //
    //     CPO|            | CPNC
    //        +--  ----  --+
    //        CPO  CPOS  CPNC
    //if only one direction is specified, we award +15 to that side and +10 to neighbouring half-corners
    //      -5 to the other sides, -20 to other corner half sides and -30 to opposite side
    //        SAN   SAM  SAN
    //        +--  ====  --+
    //    SPNS|     ^      | SPNS
    //            if this
    //    SPNS|    side    | SPNS
    //           selected
    //    SPOC|            | SPOC
    //        +--  ----  --+
    //        SPO  SPO   SPO
    double score = 0;
    if (wanted_dist) {   //0 = not specified, -1:near, +1 far, -2, +2...
        if (dist == *wanted_dist)
            score += dist_award_match;
        else
            score +=
                dist_penalty_mul * abs(dist - *wanted_dist);
    }
    if (wanted_x==0 && wanted_y!=0) {
        if (x == 0 && y == 2*wanted_y)
            score += side_award_match;
        else if (y == 2*wanted_y)
            score += side_award_neighbour;
        else if (y == wanted_y || y==0)
            score += side_penalty_neighbour_side;
        else if (y == -wanted_y)
            score += side_penalty_other_corner;
        else
            score += side_penalty_opposite;
    } else if (wanted_x!=0 && wanted_y==0) {
        if (y == 0 && x == 2*wanted_x)
            score += side_award_match;
        else if (x == 2*wanted_x)
            score += side_award_neighbour;
        else if (x == wanted_x || x==0)
            score += side_penalty_neighbour_side;
        else if (x == -wanted_x)
            score += side_penalty_other_corner;
        else
            score += side_penalty_opposite;
    } else if (wanted_x!=0 && wanted_y!=0) {
        if (sign(wanted_x) == sign(x) &&
            sign(wanted_y) == sign(y))
            score += corner_award_match;
        else if (sign(wanted_x) == sign(x))
            score += y ? corner_penalty_neighbour_corner : corner_award_neighbour_side;
        else if (sign(wanted_y) == sign(y))
            score += x ? corner_penalty_neighbour_corner : corner_award_neighbour_side;
        else if (x == 0 || y == 0)
            score += corner_penalty_other_side;
        else
            score += corner_penalty_opposite;
    }
    return score;
}

/** Scores a note placement based on overlaps with other arcs.
 * @param [in] pointto The tip of the pointer
 * @param [in] center The center of the note
 * @param [in] block_all The contour of all arcs in this region - any overlap with
 *                       these shall be penalized
 * @param [in] block_imp The contour of the important parts of the arcs in this region -
 *                       any overlap with these shall be _heavily_ penalized
 * @param cover_penalty The score we update */
void Note::CoverPenalty(const XY &pointto, const XY &center,
                               const Contour &block_all, const Contour &block_imp,
                               score_t &cover_penalty) const
{
    _ASSERT(is_float);
    const Contour cov = CoverAll(pointto, center);
    const double cov_area = cov.GetArea();
    const double cov_target_area = (cov * target->GetAreaToNote()).GetArea();
    const double cov_sng_ratio = (cov * block_all).GetArea()/ cov_area;
    const double cov_imp_ratio = (cov_target_area + (cov * block_imp).GetArea()) / cov_area;

    if (cov_sng_ratio) {
        cover_penalty += penalty_body_cover_something;
        cover_penalty += penalty_body_cover_something_mul * cov_sng_ratio;
    }
    if (cov_imp_ratio) {
        cover_penalty += penalty_body_cover_imp;
        cover_penalty += penalty_body_cover_imp_mul * cov_imp_ratio;
        //penalize extra if arrow crosses the target itself
        if (cov_target_area>1) {
            cover_penalty += penalty_body_cover_imp;
            cover_penalty += penalty_body_cover_imp_mul * (cov_target_area/cov_area);
        }
    }
    switch (cov.GetBoundingBox().RelationTo(chart->GetDrawing())) {
    case contour::REL_A_IN_HOLE_OF_B:
    case contour::REL_A_INSIDE_B:
    case contour::REL_A_IS_EMPTY:
        return; //we are fully inside the drawing area
    default:
        _ASSERT(0);
        FALLTHROUGH;
    case contour::REL_OVERLAP:
    case contour::REL_APART:
        cover_penalty += penalty_for_outside_drawing;
        break;
    }
    switch (cov.GetBoundingBox().RelationTo(chart->GetTotal())) {
    case contour::REL_A_INSIDE_B:
    case contour::REL_A_IS_EMPTY:
        return; //we are fully inside the total surface
    case contour::REL_OVERLAP:
    case contour::REL_APART:
        cover_penalty += penalty_for_outside_total;
        break;
    default: //all else is an error
        _ASSERT(0);
    }
}

//tangent is a fw tangent
/** Score a placement based on the angle the pointer hits the target.
 * Best is a 90 degree hit. There are tricky cases, such as when we hit the target
 * at a vertex of its (tip of an arrowhead, for example) or at a line-end (start of an
 * arrow).
 * @param [in] pointto The tip of the pointer
 * @param [in] center The center of the note
 * @param [in] tangent A forward tangent of the target's contour at `pointto`.
 * @param slant_penalty The score we update */
void Note::SlantPenalty(const XY &pointto, const XY &center, const XY &tangent,
                               score_t &slant_penalty) const
{
    _ASSERT(is_float);
    const XY meroleges = (tangent-pointto).Rotate90CCW().Normalize();
    const XY irany = (center-pointto).Normalize();
    const double dev_from_90 = rad2deg(acos(std::min(1.0, fabs(irany.DotProduct(meroleges))))); //between 0..90
    _ASSERT(dev_from_90>=0 && dev_from_90<=90);
    if (dev_from_90 >= 30)
        slant_penalty += penalty_for_pointer_vs_target_angle_mul *
            (dev_from_90 - 30);
}

/** Returns a point inside `c` on the `p1`->`p2` line.
 * The point should be as inside as possible, that is in the middle of the
 * largest section on the `p1`->`p2` line that falls inside `c`.
 * Returns false if no suitable point found (`p1`->`p2` does not cross `c`).*/
bool Note::GetAPointInside(const Contour &c, const XY &p1, const XY &p2, XY &ret)
{
    if (p1.test_equal(p2)) {
        if (c.IsWithin(p1)==contour::WI_OUTSIDE) return false;
        ret = p1;
        return true;
    }
    DoubleMap<bool>map(false);
    c.Cut(p1, p2, map);
    double r;
    if (!GetAPointInside(map, r)) return false;
    ret = p1 + (p2-p1)*r;
    return true;
}

/** Returns a point inside a true range in `map`.
 * The point should be as inside as possible, that is in the middle of the
 * largest true range.
 * Returns false if no suitable point found (all of `map` is false)*/
bool Note::GetAPointInside(const DoubleMap<bool> &map, double &ret)
{
    Range candidate_range(0,0); //width of 0
    double pos = -CONTOUR_INFINITY;
    do {
        double next_set = map.Till(pos);
        while (!*map.Get(next_set) && next_set < CONTOUR_INFINITY)
            next_set = map.Till(next_set);
        if (next_set == CONTOUR_INFINITY) break;
        pos = map.Till(next_set); //we do not cycle until false, we treal two true sections after one another as separate
        if (pos == CONTOUR_INFINITY) break;
        if (pos-next_set > candidate_range.Spans()) {
            candidate_range.from = next_set;
            candidate_range.till = pos;
        }
    } while (pos < CONTOUR_INFINITY);
    if (0==candidate_range.Spans()) return false;
    ret = candidate_range.MidPoint();
    return true;
}

/** Main routine for placing a note
 * This is called for notes from MscChart::CompleteParse() via MscChart::PlaceFloatingNotes(), just
 * before PostPosProcess(). This is a computation intensive trial-score-select routine.*/
#ifdef __GNUG__
__attribute__((no_sanitize("address"))) // TODO remove if invalid-pointer-pair with std::sort is resolved (std::sort() below exhibits this problem with ASAN)
#endif // __GNUG__
void Note::PlaceFloating(Canvas &/*canvas*/)
{
   if (!valid) return;
    _ASSERT(is_float);
    /* how many belts will we have around target */
    constexpr unsigned region_distances = 3;
    /* if belts starting from 0 (nearest) to region_distances-1, what shall be the
       default order of search */
    static constexpr unsigned distances_search_order[region_distances] = {1, 0, 2};
    /* What is the pixel distance from the target of the belt margins */
    static constexpr double region_distance_sizes[region_distances+1] = {0, 10, 30, 100};

    //Do a shorter alias
    constexpr unsigned RD = region_distances;
    if (!float_dist)
        float_dist = style.read().note.def_float_dist;
    if (float_dir_x==0 && style.read().note.def_float_x)
        float_dir_x = *style.read().note.def_float_x;
    if (float_dir_y==0 && style.read().note.def_float_y)
        float_dir_y = *style.read().note.def_float_y;

    //Normalize attributes
    if (unsigned(abs(*float_dist)) > RD/2)
        *float_dist = *float_dist>0 ? RD/2 : -int(RD/2);
    float_dir_x = std::max(-1, std::min(+1, float_dir_x));
    float_dir_y = std::max(-1, std::min(+1, float_dir_y));

    //This struct holds the searching pattern.
    //x_pos is +2 at the right edge of the box searching, +1 in the right third
    //+ in the middle third, -1 in the left third and -2 along the left edge.
    //"dist" can be between [0..region_distances)
    struct region_block_t {
        int x;
        int y;
        int dist = 0;
        score_t score{0,0,0};
        int map = 0;
        constexpr bool operator <(const region_block_t &o) const noexcept {return o.score<score;} //HIGHER score first
    };
    std::array<region_block_t, 24*region_distances> region_blocks = {{
        {+2,-1}, {+1,-2}, {+2, 0}, { 0,-2}, {+2,+1}, {-1,-2},
        {+1,+2}, {-2,-1}, { 0,+2}, {-2, 0}, {-1,+2}, {-2,+1} }};
    //add tie-breaking scores
    for (unsigned u=0; u<12; u++) {
        region_blocks[u].score.c = -double(u);
        region_blocks[u].dist = distances_search_order[0];
    }
    //Now we copy the above directional patterns as many times as many distances there are
    for (unsigned u=1; u<region_distances; u++)
        for (unsigned v=0; v<12; v++) {
            region_blocks[u*12+v] = region_blocks[v];
            region_blocks[u*12+v].dist = distances_search_order[u];
            region_blocks[u*12+v].score.c -= 12*double(u); //default prefs for distance: we like
        }

    //now we score these according to the note's attributes.
    for (unsigned u=0; u<RD; u++)
        for (unsigned v=0; v<12; v++) {
            region_block_t &RB = region_blocks[u*12+v];
            //score hard user preferences
            RB.score.a += ScoreRegion(float_dist, float_dir_x, float_dir_y,
                                      RB.dist - RD/2, RB.x, RB.y); //distance is normalized back to -1..+1
            ////score softer default preferences
            //RB.score.b += ScoreRegion(style.note.def_float_dist,
            //                          style.note.def_float_x.value,
            //                          style.note.def_float_y.value,
            //                          RB.dist - RD/2, RB.x, RB.y)/2;
        }

    //Now duplicate the whole shabang, and factor in which map to avoid
    //0 means to avoid all screen elements
    //1 means to avoid only the important parts
    for (unsigned u=0; u<RD*12; u++) {
        region_blocks[u+RD*12] = region_blocks[u];
        region_blocks[u].map = 0;
        region_blocks[u+RD*12].map = 1;
        region_blocks[u+RD*12].score += penalty_body_cover_something;
    }

    //OK, now sort according to score - to start scoring those regions that
    //may have a bogger chance - eliminating more of the later ones & be faster
    std::sort(region_blocks.begin(), region_blocks.end());

    //Create the region belts
    const Block total = chart->GetTotal().CreateExpand2D(halfsize);
    const XY note_gap(chart->compressGap, chart->compressGap);
    Contour region_belts[region_distances];
    const Contour *contour_target = &target->GetAreaToNote();
    if (contour_target->IsEmpty()) {
        chart->Error.Warning(file_pos.start, "The target of this note has no shape, I cannot point the note to anything. Ignoring note.");
        chart->Error.Warning(target->file_pos.start, file_pos.start, "This is the target of the note.");
        valid = false;
        return;
    }
    const std::optional<std::array<std::pair<XY, XY>, 2>> target_points = GetPointerTarget(); //call only once, as it emits errors
    //The centroid of the node is taken as the origin of placement regions, if there were no 'at' clauses.
    //Else we take the point in-between the two cut points, so that if we specify, eg, "note at <entity> [pos=up]"
    //for a long arrow, then we en up above the <entity> and not above the center of the arrow.
    const XY target_centroid = target_points
        ? Mid(target_points->at(0).first, target_points->at(1).first)
        : contour_target->Centroid();
    //For very long arrows, if the user specified an 'at' clause, limit the
    //target area to something as large as the note. This will create the belts closer
    //to the point selected by the user.
    Contour limited_contour_target;
    if (target_points) {
        //If the note is longish, adjust it to make a bit more squarish
        const XY adj_halfsize((halfsize.x*2+halfsize.y)/3, (halfsize.x+halfsize.y*2)/3);
        limited_contour_target = *contour_target * Block(target_centroid-adj_halfsize, target_centroid+adj_halfsize);
        //Do this if the remaining target is as big as the note in at least one dimension
        if (const Block &bb = limited_contour_target.GetBoundingBox()
               ; fabs(bb.x.Spans()-adj_halfsize.x*2)<1 || fabs(bb.y.Spans()-adj_halfsize.y*2)<1)
            contour_target = &limited_contour_target;
    }

    //Region belts are created via Expand2D to avoid spikes
    Contour prev = contour_target->CreateExpand2D(XY(region_distance_sizes[region_distances], region_distance_sizes[region_distances])+halfsize+note_gap);
    _ASSERT(!prev.IsEmpty());
    for (int i = region_distances-1; i>=0; i--) {
        Contour next = contour_target->CreateExpand2D(XY(region_distance_sizes[i], region_distance_sizes[i])+halfsize+note_gap);
        _ASSERT(!next.IsEmpty());
        region_belts[i] = prev - next;
        _ASSERT(!region_belts[i].IsEmpty());
        region_belts[i] *= total;  //limit to chart space. May become empty.
        prev.swap(next);
    }

    //Create three maps containing places the center of the note body can go.
    //Here we simplistically assume the body is a box of size 2*halfsize.
    //"map_all" contains a hole for all the drawn elements (expanded) around the
    //  target, plus small rectangles for non-drawn important places
    //  such as entity lines turned off.
    //  (We create an area of interest that is a box around the target including
    //   any place the center of the note can go - we care for blocking elements
    //   only here. AOI is used as a sort of limit to computation)
    //  We consider elements expanded by "halfsize": if the center of the note falls inside
    //  the map - the note will not overlap with the elements in NoteMapAll.
    //"map_imp" contains a hole for the important parts of elements. Each element
    //  specifies this during "Layout()" and we expand2d it same as for above.
    //"map_pointer_all" is inverse: it contains a positive surface for all elements
    //  inside AOI this is used to calculate how much the arrow covers
    //"map_pointer_imp" is also inverse, it just contains the important parts
    //  even the arrow should avoid it.
    const Block AOI = contour_target->GetBoundingBox().CreateExpand(region_distance_sizes[RD]).Expand2D(halfsize);

    Contour block_all_exp(total), block_imp_exp(total);
    Contour block_all, block_imp;
    for (auto i = chart->NoteBlockers.rbegin(); !(i==chart->NoteBlockers.rend()); i++) {
        if (target == *i) continue; //avoid clashes of the target with the note during arrow placement
        if ((*i)->GetAreaToDraw().GetBoundingBox().Overlaps(AOI)) {
            block_all_exp -= (*i)->GetAreaToDraw().CreateExpand2D(halfsize+note_gap);
            block_all += (*i)->GetAreaToDraw();
        }
        if ((*i)->GetAreaImportant().GetBoundingBox().Overlaps(AOI)) {
            block_imp_exp -= (*i)->GetAreaImportant().CreateExpand2D(halfsize+note_gap);
            block_imp += (*i)->GetAreaImportant();
        }
    }
    //Take out the target from pointer_map, so that anything inside it
    //does not block us
    //block_all -= contour_target;
    //block_imp += contour_target;
    //chart->DebugContours.push_back(MscChart::ContourAttr(block_all, FillAttr(ColorType(255,128,255, 128))));
    //chart->DebugContours.push_back(MscChart::ContourAttr(block_imp, FillAttr(ColorType(255,255,128, 128))));

    XY best_center;
    XY best_pointto;
    score_t best_point = worst_point;
    score_t penalty = worst_point;

    Contour contour_target_rot; //Preallocate - rotation does not change number of elements

    //We divide the belt around the target into 12 sections and 3 ("region_distances")
    //belts at ever greater distance (for near/far). We have two such sets one for
    //the important and one for all elements.
    //Process through each such section (or block)
    for (unsigned rno=0; rno<RD*24; rno++) {
        const region_block_t &RB = region_blocks[rno];
        if (RB.score <= best_point)
            break; //no chance of getting any better
        const Contour &map = RB.map ? block_imp_exp : block_all_exp;
        //Intersect the belt section with the map: get all the points
        //where the center of the note body can go.
        const Block &outer = region_belts[RD-1].GetBoundingBox();
        if (outer.IsInvalid()) continue;
        const Contour region_mask = GetRegionMask(outer, target_centroid, RB.x, RB.y) * region_belts[RB.dist];
        const Contour region = region_mask * map;
        if (region.IsEmpty()) continue;
        Contour region_rot; //Preallocate - rotation does not change number of elements

        const score_t region_penalty = penalty_for_covered_region_mul *
            (1 - region.GetArea()/region_mask.GetArea());

        //Ok, a note can be placed in this region.
        //Now find a suitable position inside the region and a
        //suitable end for the arrow that lies on the line between

        //We try to place both the body inside 'region' and its pointer
        //simultaneously. We will go by angle. The arrow of a note should preferably be
        //perpendicular to the defnodetartget line. If that is not possible we attempt
        //ever smaller angles, till we find one. Angles are in radians below.
        const double angle_step_unit = deg2rad(10);
        //starting angle is the angle specified by the sector of the belt we are in
        //it shall be zero if we want to start with a vertical down center->pos arrangement
        const double start_angle = -atan2(double(sign(RB.x)), double(sign(-RB.y)));  //deliberate swap of x & y

        //Do it differently if we have target points
        if (target_points) {
            for (unsigned tpu=0; tpu<target_points->size(); tpu++) {
                const XY tp = target_points->at(tpu).first;
                const XY tangent = target_points->at(tpu).second;
                XY c, cc;
                if (!region.TangentFrom(tp, c, cc)) continue;
                const Contour arrowspace = region + Contour(tp, c, cc);
                //if (Contour::IsResultOverlapping(arrowspace.RelationTo(contour_target, true)))
                //    continue;

                for (double angle_step = angle_step_unit,
                            angle = start_angle;
                    fabs(angle_step)<=M_PI/2;
                    angle += angle_step,
                    angle_step = -angle_step + ((angle_step > 0) ? -angle_step_unit : angle_step_unit)) {

                    //Calculate the penalty of the angle (in relation to the region)
                    score_t local_penalty = region_penalty;
                    local_penalty += (angle_step>deg2rad(10)) ?
                         penalty_for_pointer_angle_mul*rad2deg(fabs(angle_step)) :
                         neutral_point;
                    if (local_penalty < penalty) break; //we cannot get any better

                    XY center;
                    if (!GetAPointInside(region, tp, tp+XY(0,-100).Rotate(cos(-angle), sin(-angle)), center))
                        continue;

                    SlantPenalty(tp, center, tangent, local_penalty);
                    if (local_penalty < penalty) continue; //futile, try next point

                    //calc blocking elements for this region
                    const Contour region_block_all = block_all * arrowspace;
                    const Contour region_block_imp = block_imp * arrowspace;

                    CoverPenalty(tp, center, block_all, block_imp, local_penalty);
                    //if (file_pos.start.line==63)
                    //    contour::debug::Snapshot(ColorType::red().Lighter(0.2), block_all, ColorType::blue().Lighter(0.2), block_imp,
                    //                             ColorType::black(), region, EC::LINEWIDTH, 3.0, Edge(tp, center),
                    //                             EC::TITLE, std::to_string(rno)+" score:"+local_penalty.Dump());
                    if (local_penalty < penalty) continue; //futile, try next point

                    //Calculate final score
                    score_t point = RB.score;
                    point += local_penalty;
                    if (point > best_point) {
                        //OK, if we get better, replace
                        best_point = point;
                        best_center = center;
                        best_pointto = tp;
                        penalty = local_penalty;
                        //chart->DebugContours.push_back(MscChart::ContourAttr(CoverAll(canvas, best_pointto, best_center)));
                    }
                    if (!(penalty < neutral_point)) break; //we have what we want
                }//for: cylce angles
                if (!(penalty < neutral_point)) break; //we have what we want
            }//for: cylce through tp's
        } else { //no target points
            //"arrowspace" contains all the possible space the arrow can go through
            XY c[2], cc[2];
            if (!region.TangentFrom(*contour_target, c, cc)) continue;
            const Contour arrowspace = (region + *contour_target + Contour(cc[0], cc[1], c[1]) + Contour(c[1], c[0], cc[0])).ClearHoles();
            Contour arrowspace_rot; //Preallocate - rotation does not change number of elements

            //calc blocking elements for this region
            const Contour region_block_all = block_all * arrowspace;
            const Contour region_block_imp = block_imp * arrowspace;

            //chart->DebugContours.push_back(MscChart::ContourAttr(arrowspace, FillAttr(ColorType(0,0,0, 128))));
            //chart->DebugContours.push_back(MscChart::ContourAttr(region, FillAttr(ColorType(255,0,0, 128))));
            //chart->DebugContours.push_back(MscChart::ContourAttr(block_imp, FillAttr(ColorType(255,128,255, 128))));

            //From here we try to place both the body inside 'region' and its pointer
            //simultaneously. We go by angle. The arrow of a note should preferably be
            //perpendicular to the defnodetartget line. If that is not possible we attempt
            //ever smaller angles, till we find one. Angles are in radians below.
            for (double angle_step = angle_step_unit,
                        angle = start_angle;
                fabs(angle_step)<=M_PI/2;
                angle += angle_step,
                angle_step = -angle_step + ((angle_step > 0) ? -angle_step_unit : angle_step_unit)) {

                //The angle penalty is added if we deviate much from the angle suitable
                //for this region. It is independent of the chart, depends only on "angle"
                //and as we cycle in the angle for cycle it only becomes worse.
                //This is split from other penalties, so that we can stop earlier in this
                //cycle, when other penalties are better than the angle penalty.
                score_t local_penalty = region_penalty;
                local_penalty += penalty_for_pointer_angle_mul*rad2deg(fabs(angle-start_angle));

                if (local_penalty <= penalty) break; //we cannot get any better

                XY pointto, tangent, center;

                const double sa = sin(angle);
                const double ca = cos(angle);
                //We rotate the tangent line and "region" by angle and see if their
                //x coordinates overlap.
                contour_target_rot = *contour_target; //Normally involves no memory re-alloc, as contour_target_rot is already set from previous round
                contour_target_rot.Rotate(ca, sa);   //in-place rotate
                region_rot = region;                 //Normally involves no memory re-alloc since previous for cycle
                region_rot.Rotate(ca, sa);
                //if (!contour_target_rot.GetBoundingBox().x.Overlaps(region_rot.GetBoundingBox().x)) continue;
                //if there is an overlap, we may find a good placement else go to next angle
                //region_rot or contour_target_rot may not be contiguous
                //Calculate the x ranges of region where we can place the arrow
                DoubleMap<int> region_ranges(0);
                //Add the rotated region's pieces to the region ranges
                for (unsigned ru = 0; ru<region_rot.size(); ru++)
                    region_ranges.Set(region_rot[ru].GetBoundingBox().x, 10000);
                //Warning contour_target_rot may have multiple overlapping ranges!
                for (unsigned ctu = 0; ctu<contour_target_rot.size(); ctu++)
                    region_ranges.Add(contour_target_rot[ctu].GetBoundingBox().x, 1);
                //OK, we are interested in where the value is greater than "10001" (both cover)
                DoubleMap<bool> region_ranges_bool(false);
                bool was = false, running=false;
                for (auto i = ++region_ranges.begin(); i!=region_ranges.end(); i++)
                    if ((i->second>=10001) != running) {
                        running = i->second >= 10001;
                        region_ranges_bool.Set(i->first, running);
                        was |= running;
                    }
                if (!was)    //rotated region and contour has no overlapping x-axis range,
                    continue; //we will never find a match with this angle

                //Now try to find a place first avoiding all visible blockers,
                //then only the important ones
                //We set success to true if we find a non-blocking "pointto" and "center",
                //but we determine some "pointto" and "center", in all the cases
                bool success = false;
                for (unsigned bl = 0; bl<2; bl++) {
                    DoubleMap<bool> canplacehere(region_ranges_bool);
                    //rotate the map, which contains the blocking contours
                    const Contour blockers_rot = (bl==0 ? region_block_all : region_block_imp).CreateRotated(ca, sa);
                    //take each independent contour and substract their x extent
                    //from the places to consider
                    for (unsigned u = 0; u<blockers_rot.size(); u++)
                        canplacehere.Set(blockers_rot[u].GetBoundingBox().x, false);
                    //Now canplacehere is true where we can place a pointer
                    canplacehere.Prune(); //merge neighbouring sections
                    XY pointto_rot;
                    if (GetAPointInside(canplacehere, pointto_rot.x))
                        success = true; //we found a range, where there is no overlap
                    else {
                        //there is no position without some overlap, pick a good position
                        //without considering blockers
                        if (!GetAPointInside(region_ranges_bool, pointto_rot.x)) {
                            _ASSERT(0);
                            continue;
                        }
                    }
                    //now find center
                    if (!GetAPointInside(region_rot, XY(pointto_rot.x,0), XY (pointto_rot.x,100), center)) {
                        //_ASSERT(0); //This happens in 'tests/notes/boxes_note.signalling' So I comment it out.
                        continue;
                    }
                    //finally, calculate pointto_rot.y. make a cut in "region_rot" and
                    //select the point closer to "center"
                    std::pair<XY,XY> fromtill[2];
                    const Range r = contour_target_rot.CutWithTangent(center, center+XY(0,100), fromtill[0], fromtill[1]);
                    _ASSERT(!r.IsInvalid());
                    _ASSERT(contour::test_equal(fromtill[0].first.x, pointto_rot.x));
                    _ASSERT(contour::test_equal(fromtill[1].first.x, pointto_rot.x));
                    const unsigned index = fabs(r.from)>fabs(r.till); //the point closer to "center"
                    pointto_rot.y = fromtill[index].first.y;
                    //Now we have a pointto_rot, find our corresponding center_rot
                    //(on the same x coordinate, but somewhere inside region_rot, as close to ideal as we can)
                    //rotate back
                    pointto = pointto_rot.Rotate(ca, -sa);
                    tangent = fromtill[index].second.Rotate(ca, -sa);
                    center.Rotate(ca, -sa);
                    center.Round(); //so that we avoid rounding mismatches
                    if (success) break; //we have found a good place, not blocking either of the maps
                } //for: blockers
                //Now we have a valid "center" and "pointto" with their
                //"local_coverage_penalty" calculated.
                //Calculate penalty if note body or pointer covers something.
                //chart->DebugContours.push_back(MscChart::ContourAttr(Contour(pointto,5), LineAttr(), FillAttr(ColorType(255,0,0, 50))));
                SlantPenalty(pointto, center, tangent, local_penalty);
                if (local_penalty < penalty) continue; //futile, try next angle
                CoverPenalty(pointto, center, block_all, block_imp, local_penalty);
                if (local_penalty < penalty) continue; //futile, try next angle

                //Calculate final score
                score_t point = RB.score;
                point += local_penalty;
                if (point > best_point) {
                    //OK, if we get better, replace
                    best_point = point;
                    best_center = center;
                    best_pointto = pointto;
                    penalty = local_penalty;
                    //chart->DebugContours.push_back(MscChart::ContourAttr(CoverAll(canvas, best_pointto, best_center)));
                }
            }  //for: angle
        }//else: we had no target points
    } //for: regions
    if (best_point <= worst_point) {
        //we did not succeed. Pick any point from the first region
        const region_block_t &RB = region_blocks[0];
        Contour region_mask = GetRegionMask(region_belts[RD-1].GetBoundingBox(), target_centroid, RB.x, RB.y) * region_belts[RB.dist];
        if (region_mask.IsEmpty()) {
            Contour prev2 = contour_target->GetBoundingBox().CreateExpand2D(XY(region_distance_sizes[region_distances], region_distance_sizes[region_distances])+halfsize+note_gap);
            _ASSERT(!prev2.IsEmpty());
            for (int i = region_distances-1; i>=0; i--) {
                Contour next = contour_target->GetBoundingBox().CreateExpand2D(XY(region_distance_sizes[i], region_distance_sizes[i])+halfsize+note_gap);
                _ASSERT(!next.IsEmpty());
                region_belts[i] = prev2 - next;
                _ASSERT(!region_belts[i].IsEmpty());
                //Here we do not limit the belts to chart space
                prev2.swap(next);
            }
            region_mask = GetRegionMask(region_belts[RD-1].GetBoundingBox(), target_centroid, RB.x, RB.y) * region_belts[RB.dist];
        }
        if (region_mask.IsEmpty()) {
            //Not successful
            chart->Error.Error(file_pos.start, "Internal error: could not place this note. Sorry.");
            valid = false;
            return;
        } else if (target_points) {
            best_center = region_mask.Centroid();
            if (best_center.Distance(target_points->front().first) < best_center.Distance(target_points->front().second))
                best_pointto = target_points->front().first;
            else
                best_pointto = target_points->front().second;
        } else {
            //if region is empty we just do it to get "any" placement
            //pick center of the (covered) region and get a point
            best_center = region_mask.Centroid();
            const XY c = contour_target->Centroid();
            std::pair<XY, XY> from, till;
            if (contour_target->CutWithTangent(c, best_center, from, till).IsInvalid()) {
                _ASSERT(0);
            }
            best_pointto = till.first;
        }
    }
    pos_center = best_center;
    point_to = best_pointto;
    //now a hack: decrease halfsize by half a linewidth, from now on CoverAll
    //is assumed to return the midline.
    halfsize.x -= style.read().line.LineWidth()/2;
    halfsize.y -= style.read().line.LineWidth()/2;
    area = CoverAll(best_pointto, best_center);
    area_important = area;
    chart->NoteBlockers.Append(this);
}

/** Main routine to place a comment
 * This is called from Layout() of the target via its LayoutCommentsHelper().
 * @param canvas The canvas we use for geometry.
 * @param cover We add the cover of this comment to this AreaList
 * @param y We place the comment at this vertical location, we add our height to it at return */
void Note::PlaceSideTo(Canvas &canvas, AreaList *cover, double &y)
{
    if (!valid) return;
    _ASSERT(!is_float);
    _ASSERT(style.read().side && *style.read().side != ESide::END);
    yPos = y;
    //reflow if needed
    const double space = *style.read().side == ESide::LEFT ?
        chart->XCoord(chart->LNote->pos) - 2*chart->sideNoteGap :
        chart->XCoord(chart->EndEntity->pos) - chart->XCoord(chart->RNote->pos) - 2*chart->sideNoteGap;
    if (parsed_label.IsWordWrap()) {
        const double overflow = parsed_label.Reflow(canvas, chart->Shapes, space);
        OverflowWarning(overflow,
            "Use 'hscale=auto' or add space via 'hspace "+
            string(*style.read().side == ESide::LEFT ? "left" : "right")+
            " comment <number>'.", nullptr, nullptr);
    } else {
        CountOverflow(space);
    }
    if (style.read().side == ESide::LEFT)
        area = parsed_label.Cover(chart->Shapes, chart->sideNoteGap,
                                  chart->XCoord(chart->LNote->pos)-chart->sideNoteGap,
                                  yPos);
    else
        area = parsed_label.Cover(chart->Shapes, chart->XCoord(chart->RNote->pos) + chart->sideNoteGap,
                                  chart->XCoord(chart->EndEntity->pos) - chart->sideNoteGap,
                                  yPos);
    area.arc = this;
    height = area.GetBoundingBox().y.till - y + chart->arcVGapBelow;
    y += height;
    if (cover)
        *cover += GetCover4Compress(area);
}

//Called during the Layout process.
//But we shall only react for end-notes, side comments are
//shifted together with their target, floating notes are not even laid out yet.
void Note::ShiftBy(double y)
{
    if (!is_float && style.read().side == ESide::END)
        LabelledArc::ShiftBy(y);
}


//Called when a comment on the side is shifted together with its target
void Note::ShiftCommentBy(double y)
{
    _ASSERT(!is_float);
    _ASSERT(style.read().side != ESide::END);
    LabelledArc::ShiftBy(y);
}


void Note::RegisterLabels()
{
    if (is_float) {
        const double w2 = halfsize.x - style.read().line.LineWidth();
        chart->RegisterLabel(parsed_label, LabelInfo::NOTE,
            pos_center.x-w2, pos_center.x+w2,
            pos_center.y-halfsize.y+style.read().line.LineWidth());

    } else switch (*style.read().side) {
    case ESide::LEFT:
        chart->RegisterLabel(parsed_label, LabelInfo::COMMENT,
            chart->sideNoteGap,
            chart->XCoord(chart->LNote->pos)-chart->sideNoteGap,
            yPos);
        break;
    case ESide::RIGHT:
        chart->RegisterLabel(parsed_label, LabelInfo::COMMENT,
            chart->XCoord(chart->RNote->pos) + chart->sideNoteGap,
            chart->XCoord(chart->EndEntity->pos) - chart->sideNoteGap,
            yPos);
        break;
    case ESide::END:
        chart->RegisterLabel(parsed_label, LabelInfo::COMMENT,
            chart->sideNoteGap,
            chart->XCoord(chart->EndEntity->pos) - chart->sideNoteGap,
            yPos);
        break;
    case ESide::INVALID:
        _ASSERT(0);
        break;
    }
}


void Note::CollectIsMapElements(Canvas &canvas)
{
    if (is_float) {
        const double w2 = halfsize.x - style.read().line.LineWidth();
        parsed_label.CollectIsMapElements(chart->ismapData, canvas, chart->Shapes,
            pos_center.x-w2, pos_center.x+w2,
            pos_center.y-halfsize.y+style.read().line.LineWidth());

    } else switch (*style.read().side) {
    case ESide::LEFT:
        parsed_label.CollectIsMapElements(chart->ismapData, canvas, chart->Shapes,
            chart->sideNoteGap,
            chart->XCoord(chart->LNote->pos)-chart->sideNoteGap,
            yPos);
        break;
    case ESide::RIGHT:
        parsed_label.CollectIsMapElements(chart->ismapData, canvas, chart->Shapes,
            chart->XCoord(chart->RNote->pos) + chart->sideNoteGap,
            chart->XCoord(chart->EndEntity->pos) - chart->sideNoteGap,
            yPos);
        break;
    case ESide::END:
        parsed_label.CollectIsMapElements(chart->ismapData, canvas, chart->Shapes,
            chart->sideNoteGap,
            chart->XCoord(chart->EndEntity->pos) - chart->sideNoteGap,
            yPos);
        break;
    case ESide::INVALID:
        _ASSERT(0);
        break;
    }
}


void Note::Draw(Canvas &canvas, EMscDrawPass pass) const
{
    if (!valid || pass!=draw_pass) return;
    if (is_float) {
        Contour cover;
        if (style.read().note.pointer == NoteAttr::ARROW) {
            cover = CoverBody(pos_center);
            if (canvas.does_graphics()) {
                const Range r = cover.CreateExpand(style.read().line.Spacing()).Cut(point_to, pos_center);
                const double len = r.from * point_to.Distance(pos_center);
                style.read().arrow.read().
                    TransformCanvasForAngle(rad2deg(atan2(-(pos_center-point_to).y, -(pos_center-point_to).x)),
                                            canvas, point_to.x, point_to.y);
                std::vector<double> v(2), a(2);
                v[0] = point_to.x - len; v[1] = point_to.x;
                a[0] = a[1] = 0;
                const Contour clip = style.read().arrow.read().
                    ClipForLine(point_to, 0, true, false, EArrowEnd::END,
                                style.read().line, style.read().line);
                canvas.ClipInverse(clip);
                canvas.Line(point_to, point_to - XY(len, 0), style.read().line);
                canvas.UnClip();
                style.read().arrow.read().Draw(canvas, point_to, 0, true, false, EArrowEnd::END, style.read().line, style.read().line);
                style.read().arrow.read().UnTransformCanvas(canvas);
            } else {
                Path line = { pos_center, point_to };
                line.ClipRemove(cover, true);
                canvas.Add(GSPath(line, style.read().line, &style.read().arrow.read().GetArrowHead(true, false, EArrowEnd::END, true)));
            }
        } else
            cover = CoverAll(point_to, pos_center);
        if (canvas.does_graphics()) {
            canvas.Shadow(cover.CreateExpand(style.read().line.Spacing()), style.read().shadow);
            canvas.Fill(cover.CreateExpand(-style.read().line.Spacing()), style.read().fill);
            canvas.Line(cover, style.read().line);
            const double w2 = halfsize.x - style.read().line.LineWidth();
            parsed_label.Draw(canvas, chart->Shapes, pos_center.x-w2, pos_center.x+w2,
                              pos_center.y-halfsize.y+style.read().line.LineWidth());
        } else {
            const Block textbox = Block(pos_center-halfsize, pos_center+halfsize).Expand(-style.read().line.LineWidth());
            canvas.Add(GSShape(std::move(cover), style.read().line, style.read().fill, style.read().shadow, textbox, parsed_label));
        }
        return;
    } else switch (*style.read().side) {
    default:
        _ASSERT(0);
        return;
    case ESide::LEFT:
        parsed_label.Draw(canvas, chart->Shapes, chart->sideNoteGap,
                          chart->XCoord(chart->LNote->pos)-chart->sideNoteGap,
                          yPos);
        return;
    case ESide::RIGHT:
        parsed_label.Draw(canvas, chart->Shapes, chart->XCoord(chart->RNote->pos) + chart->sideNoteGap,
                          chart->XCoord(chart->EndEntity->pos) - chart->sideNoteGap,
                          yPos);
        return;
    case ESide::END:
        parsed_label.Draw(canvas, chart->Shapes, chart->sideNoteGap,
                          chart->XCoord(chart->EndEntity->pos) - chart->sideNoteGap,
                          yPos);
        return;
    }
}


//////////////////////////////////////////////////////////////////////

void EndNoteSeparator::Layout(Canvas &/*canvas*/, AreaList * /*cover*/)
{
    yPos = 0;
    height = chart->nudgeSize;
    vspacing = 0;
    const Block b(chart->GetTotal().x.from, chart->GetTotal().x.till, 0, chart->nudgeSize);
    area.mainline = b;
}

void EndNoteSeparator::PostPosProcess(Canvas &/*cover*/, Chart * /*ch*/)
{
    //Stop the potential fill of the side comment lanes
    MscStyleCoW no_fill; //empty style
    no_fill.write().fill.color = ColorType{}; //INVALID
    chart->LNote->status.ApplyStyle(yPos, no_fill);
    chart->RNote->status.ApplyStyle(yPos, no_fill);

    //turn all entities off
    for (auto pEntity : chart->ActiveEntities)
        pEntity->status.SetStatus(yPos, EEntityStatus::SHOW_OFF);
}

void EndNoteSeparator::Draw(Canvas &canvas, EMscDrawPass pass) const
{
    if (pass != EMscDrawPass::DEFAULT) return;
    const XY A(chart->GetTotal().x.from+chart->sideNoteGap*2, yPos + height/2);
    const XY B(chart->GetDrawing().x.till-chart->sideNoteGap*2, yPos + height/2);
    const LineAttr line(ELineType::SOLID,
                        *chart->LNote->status.GetStyle(yPos).read().line.color,
                        1, ECornerType::NONE, 0);
    if (canvas.does_graphics())
        canvas.Line(A, B, line);
    else
        canvas.Add(GSPath({ A,B }, line));
}

///<Location of second string

