/*
    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 blockcsh.cpp The definition for the BlockCsh class and coloring for the "block" language.
* @ingroup libblock_files */


#include <cstring>
#include "blockcsh.h"
#include "blockstyle.h"
#include "blockchart.h"
#include "utf8utils.h"
#include "block_parser_csh.h"

using namespace block;

BlockCsh::BlockCsh(Csh::FileListProc proc, const LanguageCollection* languages) :
    Csh(BlockContext(true, EContextParse::NORMAL, EContextCreate::PLAIN, FileLineCol()), proc, languages),
    hadProcReplay(false)
{
    ForbiddenStyles.erase("row");
    ForbiddenStyles.erase("col");
    FillNamesHints();
}

void BlockCsh::CreateBlocksFromLast(const CshStringWithPosList* l) {
    if (!l || l->size()<2) return;
    //Here we assume that for the last element of the list we already have created names in 'blocks'.
    //We ignore names with '.' in them (those come from a multi command).
    //1. Find the 'last' such element.
    const auto last = std::find_if(l->rbegin(), l->rend(), [](const CshStringWithPos& sp) { return sp.name.find('.')==std::string::npos; });
    if (last != l->rend()) {
        const std::string prefix = PrefixName(last->name)+".";
        //Now iterate backwards
        for (auto i = std::next(last); i!=l->rend(); i++)
            if (i->name.find('.')==std::string::npos) //only names without a dot
                //Search Blocks for names starting with 'prefix'
                for (auto& [name, shape] : Blocks)
                    if (name.starts_with(prefix)) {
                        auto [itr, inserted] = Blocks.insert({ PrefixName(i->name + name.substr(prefix.size()-1)), shape });
                        if (!inserted)
                            itr->second = -2;
                    }
    }
    //Now create a block for all names having a dot.
    for (const CshStringWithPos& sp : *l)
        if (size_t pos = sp.name.find('.'); pos!=std::string::npos) {
            const auto i = Blocks.find(PrefixName(sp.name.substr(0, pos)));
            const int shape = i==Blocks.end() ? -1 : i->second;
            auto [itr, inserted] = Blocks.insert({ PrefixName(sp.name), shape });
            if (!inserted)
                itr->second = -2;
            //These names should come from a multi
            [[maybe_unused]] static const char* names[] = { "front", "back", "2", "3", "4", "5", "6", "7", "8", "9" };
            _ASSERT(std::find(std::begin(names), std::end(names), sp.name.substr(pos+1))!=std::end(names));
        }
}

void BlockCsh::DeleteBlock(std::string_view name) {
    auto i = Blocks.find(PrefixName(name));
    if (i!=Blocks.end() && i->second>=-1) //if the shape is valid this Block is mentioned only once, so we can delete it.
        Blocks.erase(i);
}

void BlockCsh::Templatize(std::string_view name, const CshPos &p) {
    std::string pref = PrefixName(name);
    auto i = Blocks.find(pref);
    if (i==Blocks.end()) { _ASSERT(0); return; }
    auto& [bl, sh] = Templates[pref];
    _ASSERT(bl.empty());
    sh = i->second;
    Blocks.erase(i);
    pref.push_back('.');
    for (auto i = Blocks.begin(); i!=Blocks.end(); /*nope*/)
        if (!i->first.starts_with(pref)) { i++; continue; }
        else {
            bl.insert({ i->first.substr(pref.size()), i->second });
            if (i->second<-1) i++;     //This name is used multiple times, keep it in Blocks
            else i = Blocks.erase(i);  //This name is used only once, so move it to the template
        }
    for (CshEntry& e : CshList) {
        if (e.IsWithin(p) && e.color==COLOR_ENTITYNAME_FIRST)
            e.color = COLOR_MARKERNAME;
        if (e==p) break;
    }
}

void BlockCsh::Templatize(gsl::owner<CshStringWithPosList*> l) {
    if (!l) return;
    for (const CshStringWithPos& sp : *l)
        if (sp.name.find('.')==std::string::npos)
            Templatize(sp.name, sp.file_pos);
    delete l;
}

bool BlockCsh::Copy(std::string_view from, std::string_view to, const CshPos& from_pos, const CshPos& to_pos) {
    //drop any prefix from 'to'
    if (const size_t pos = to.find_last_of('.'); pos != to.npos)
        to.remove_prefix(pos + 1);
    const auto t = SearchName(Templates, from, false, false);
    switch (t.first) {
    case -2: //partially typed template name
        break; //try finding a fully matching block name
    case -3:
        AddCSH_Error(from_pos, "More than one template of this name found.");
        return true;
    case -4:
        break;
    default:
        _ASSERT(0);
        break;
    case -1:
    instantiate_template:
        AddCSH_NewBlock(to_pos, to, /*shape=*/t.second->second.second);
        std::string full_to = PrefixName(to);
        full_to.push_back('.');
        for (auto& [bn, sh] : t.second->second.first) {
            auto [itr, inserted] = Blocks.insert({ full_to+bn, sh});
            if (!inserted) itr->second = -2;
        }
        if (from_pos != to_pos) {
            AddCSH(from_pos, t.first == -1 ? COLOR_MARKERNAME : COLOR_MARKERNAME_PARTIAL);
            if (t.first==-2)
                AddCSH_Error(from_pos, "No template of this name found, maybe '"+t.second->first+"'");
        }
        return false;
    }
    const auto b = SearchName(Blocks, from, false, true);
    switch (b.first) {
    case -3:
        AddCSH_Error(from_pos, "More than one block of this name found.");
        return true;
    default:
        _ASSERT(0); FALLTHROUGH;
    case -4:
        AddCSH_Error(from_pos, "No block of this name found.");
        return true;
    case -2: //partially typed block name
        if (t.first == -2) goto instantiate_template;
        FALLTHROUGH;
    case -1:
        //Copy a block
        AddCSH_NewBlock(to_pos, to, /*shape=*/b.second->second);
        BlockList add;
        std::string full_from = PrefixName(from);
        std::string full_to = PrefixName(to);
        full_from.push_back('.');
        full_to.push_back('.');
        for (auto& [bn, sh] : Blocks)
            if (bn.starts_with(full_from)) {
                auto [itr, inserted] = add.insert({ full_to + bn.substr(full_from.size()), sh });
                if (!inserted) itr->second = -2;
            }
        Blocks.merge(add); //elements of 'add' already in 'Blocks' remain in 'add'
        for (auto& [bl, _] : add) Blocks[bl] = -2;
        if (from_pos != to_pos) {
            AddCSH(from_pos, b.first == -1 ? COLOR_ENTITYNAME : COLOR_ENTITYNAME_PARTIAL);
            if (b.first == -2)
                AddCSH_Error(from_pos, "No block of this name found, maybe '" + b.second->first + "'");
        }
        return false;
    }
}

gsl::owner<CshStringWithPosList*> BlockCsh::MakeMulti(int num, gsl::owner<CshStringWithPosList*> l) {
    if (!l || l->empty()) return l;
    const size_t orig = l->size();
    for (size_t u = 0; u<orig; u++) {
        l->emplace_back(l->at(u).name+".front", l->at(u).file_pos);
        l->emplace_back(l->at(u).name+".back", l->at(u).file_pos);
        for (int n = 2; n<num; n++)
            l->emplace_back(l->at(u).name+"."+ std::to_string(n), l->at(u).file_pos);
    }
    return l;
}


void BlockCsh::FillNamesHints()
{
    BlockChart::AttributeNames(*this, false);
    MoveHintsToOptionNames();

    AddKeywordsToHints(true, true, true);
    MoveHintsToKeywordNames();

    //Put all element attributes as hints
    BlockBlock::AttributeNames(EBlockType::Box, *this);
    BlockBlock::AttributeNames(EBlockType::Row, *this);
    BlockBlock::AttributeNames(EBlockType::Column, *this);
    BlockBlock::AttributeNames(EBlockType::Text, *this);
    Arrow::AttributeNames(*this);
    ArrowLabel::AttributeNames(*this);
	//Add 'shape' for running style
	AddToHints(CshHint(HintPrefix(COLOR_ATTRNAME) + "shape",
					   "Set the shape of the entity.",
					   EHintType::ATTR_NAME));
	MoveHintsToAttrNames();
}

/** Adds keywords to hints for box, row, column, text, shape, left right, etc.*/
void BlockCsh::AddKeywordsToHints(bool include_dir, bool include_join_around, bool include_multi_copy)
{
    if (include_multi_copy) {
        AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "multi",
                           "Make a subsequent block appear in several instances.",
                           EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
        AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "copy",
                           "Create a copy of an existing block or template with potential modifications.",
                           EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
    }
    if (include_join_around) {
        AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "join",
                           "Join several existing block into a single contour.",
                           EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
        AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "around",
                           "Create a new block enclosing several other blocks.",
                           EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
    }
    AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "box",
        "Define a new block of box type.",
        EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
    AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "row",
        "Invisible construct holding a series of blocks left to right.",
        EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
    AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "col",
        "Invisible construct holding a series of blocks top to bottom.",
        EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
    AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "text",
        "Define a new block with text only.",
        EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
    AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "shape",
        "Define a new block with a specific shape.",
        EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
    AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "space",
                       "Add a bit of an empty space. (10 points by default, add a number.)",
                       EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
    if (include_dir) {
        AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "leftof",
                           "Makes the block to be laid out left of (an)other block(s).",
                           EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
        AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "rightof",
                           "Makes the block to be laid out right of (an)other block(s).",
                           EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
        AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "above",
                           "Makes the block to be laid out above the (an)other block(s).",
                           EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
        AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "below",
                           "Makes the block to be laid out below  the (an)other block(s).",
                           EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
        AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "left",
                           "Makes the block align to the left side of (an)other block(s).",
                           EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
        AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "right",
                           "Makes the block align to right side of (an)other block(s).",
                           EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
        AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "top",
                           "Makes the block align to the top of (an)other block(s).",
                           EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
        AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "bottom",
                           "Makes the block align to the bottom of (an)other block(s).",
                           EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
    }
}

void BlockCsh::AddLineBeginToHints()
{
    Csh::AddLineBeginToHints();
    AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "usedesign",
                       "Apply settings of an existing design to the one under definition.",
                       EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
    AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "template",
                       "Define a block template that can be used to create several copies of.",
                       EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
    AddUseKeywordsToHints(USE_KEYWORD_NONE);
    AddMarkExtendToHints(true, true);
    AddKeywordsToHints(true, true, true);
    AddEntityNamesAtTheEnd("Make an arrow or line start or end at block %s.");
    AddOptionsToHints();
    AllowAnything();
}

/** Add the 'use' keyword to the hints and all modifier that is NOT included in 'e'.*/
void BlockCsh::AddUseKeywordsToHints(EUseKeywords e)
{
    if (!(USE_KEYWORD_BLOCKS & e))
        AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "blocks",
                           "Makes the following 'use' command apply to blocks (only).",
                           EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
    if (!(USE_KEYWORD_ARROWS & e))
        AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "arrows",
                           "Makes the following 'use' command apply to arrows (only).",
                           EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
    AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "use",
                       "Apply a style or a list of attributes to the running style. "
                       "The running style is added to every element (or just arrows or blocks) and serves as a convenient shorthand to "
                       "set an attribute to the same value in all blocks and/or arrows. If an attribute set in the "
                       "running style for both arrows and blocks applies only to either blocks or to arrows, it is silently ignored for the other kind.",
                       EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
}

void BlockCsh::AddCloneActionKeywordsToHints()
{
    static const char * const names_descr[] = {
        "invalid", nullptr,
        "drop", "Drop a named block.",
        "add", "Add a new block or arrow.",
        "update", "Add attributes and/or change content of a named block.",
        "replace", "Replace a named block with something else.",
        "move", "Move a named block to another position in the content list.",
        "recursive", "Apply attributes to all arrows and/or blocks inside the copied block.",
        ""};
    AddUseKeywordsToHints();
    AddToHints(names_descr, HintPrefix(COLOR_KEYWORD), EHintType::KEYWORD);
}

void BlockCsh::AddBeforeToHints()
{
    AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "before",
                       "Specify the block before the item shall be added or moved.",
                       EHintType::KEYWORD, true));
}

void BlockCsh::AddMarkExtendToHints(bool mark, bool extend)
{
    if (mark)
        AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "mark",
                           "Create a marker on an arrow at a given position.",
                           EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
    if (extend)
        AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "extend",
                           "Makes an arrow label or marker be in before the start of the arrow or beyond its end.",
                           EHintType::KEYWORD, true, CshHintGraphicCallbackForKeywords));
}


/** Callback for drawing a port position in hintboxes for boxes.
 * 'p' shall be between 0..7 for various ports.
 * @ingroup hintpopup_callbacks*/
bool CshHintGraphicCallbackForBoxPorts(Canvas *canvas, CshHintGraphicParam p, CshHintStore &csh)
{
    if (!csh.pShapes) return false;
    const size_t port = size_t(p);
    //x, y coordinates and compass point (dir)
    static const double ports[] = {
        0.5,   0,   0, //top
        1,   0,  45, //topright
        1, 0.5,  90, //right
        1,   1, 135, //bottomright
        0.5,   1, 180, //bottom
        0,   1, 225, //bottomleft
        0, 0.5, 270, //left
        0,   0, 315  //topleft
    };
    if (port>7) return false;
    const Block hint(0, 1, 0, 1);
    const double r = std::min(HINT_GRAPHIC_SIZE_X/hint.x.Spans(), HINT_GRAPHIC_SIZE_Y/hint.y.Spans())*0.5;
    //The origin of the part visible in the hint window in the space of the Shape
    const XY orig(hint.x.from + (hint.x.Spans() - HINT_GRAPHIC_SIZE_X/r)/2,
                  hint.y.from + (hint.y.Spans() - HINT_GRAPHIC_SIZE_Y/r)/2);
    Block max = hint;
    max.Shift(-orig).Scale(r);
    canvas->Line(LineAttr().CreateRectangle_Midline(max), LineAttr());
    Shape::Port pp{{ports[3*port], ports[3*port+1]}, ports[3*port+2]};
    pp.xy += -orig;
    pp.xy *= r;
    canvas->Fill(Contour(pp.xy, 2), FillAttr(ColorType::red(), EGradientType::OUTWARD));
    if (pp.dir>=0) {
        const XY dir = XY(0, -1).Rotate(cos(pp.dir/180*M_PI), sin(pp.dir/180*M_PI))*
            (HINT_GRAPHIC_SIZE_X+HINT_GRAPHIC_SIZE_Y); //very long
        canvas->Line(Path({pp.xy, pp.xy+dir}), LineAttr(ELineType::SOLID, ColorType::red()));
    }
    return true;
}


/** Port names for a shape, -1 being the box, smaller numbers don't add anything.*/
void BlockCsh::AddPortsToHints(int shape)
{
    if (shape==-1) {
        static const char portnames[][12] = {
            "top", "topright", "right", "bottomright",
            "bottom", "bottomleft", "left", "topleft"
        };
        for (unsigned u=0; u<8; u++)
            AddToHints(CshHint(HintPrefix(COLOR_ATTRVALUE) + portnames[u],
                               "Use this port of the box.",
                               EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForBoxPorts,
                               CshHintGraphicParam(u)));
    } else
        Csh::AddPortsToHints(shape);
}

void BlockCsh::AddWhatToUpdateToHints(bool include_keywords)
{
    if (include_keywords) {
        AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "blocks",
                           "Update attributes of all blocks recursively.",
                           EHintType::KEYWORD));
        AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "arrows",
                           "Update attributes of all arrows recursively.",
                           EHintType::KEYWORD));
        AddToHints(CshHint(HintPrefix(COLOR_KEYWORD) + "all",
                           "Update attributes of all elements recursively.",
                           EHintType::KEYWORD));
    }
    std::string n = GetCurrentPrefix()+".";
    for (auto i = Blocks.lower_bound(n); i!=Blocks.end() && i->first.starts_with(n); i++)
        AddToHints(CshHint(StrCat(HintPrefix(COLOR_ENTITYNAME), std::string_view(i->first).substr(n.size())),
                            "A named block contained in the copied block.", EHintType::ENTITY));
    hintStatus = HINT_READY;

}

void BlockCsh::AddDesignOptionsToHints()
{
    BlockChart::AttributeNames(*this, true);
}

void BlockCsh::AddOptionsToHints()
{
    BlockChart::AttributeNames(*this, false);
}


void BlockCsh::BeforeYaccParse(std::string&& input, CharByteIndex first_p, CharByteIndex cursor_p)
{
    Csh::BeforeYaccParse(std::move(input), first_p, cursor_p);
    parent_name.clear();
    prefix_at_hint_loc.clear();
    Context2.clear();
    Blocks.clear();
    BlockMentions.clear();
    hadProcReplay = false;
    Blocks["prev"] = -2; //-2: invalid shape number to avoid hinting ports
    Blocks["next"] = -2;
    Blocks["first"] = -2;
    Blocks["last"] = -2;
    Context2.emplace_back("", true, -1); //pedantic will be later set in ParseText()
}

/** Parse chart text for color syntax and hint info
 *
 * @param [in] input The chart text
 * @param [in] first_char Position of the first character of 'input' in the file we parse.
 *             If we parse a whole file, it shall be {0,0}.
 * @param [in] cursor_p The current position of the cursor.
 * @param [in] pedantic The initial value of the pedantic chart option.*/
void BlockCsh::ParseText(std::string&& input, CharByteIndex first_char, CharByteIndex cursor_p, bool pedantic)
{
    //initialize data struct
    BeforeYaccParse(std::move(input), first_char, cursor_p);
    Context2.back().pedantic = pedantic;
    //call parsing
    sv_reader<true> reader(input_text, first_char);
    block_csh_parse(*this, reader); //return value ignored
    //Tidy up afterwards
    AfterYaccParse();
}

/** This is called, when we hint an attribute value containing "A+B@top"-like attributes.
 * specifically, alignment attributes, via and size attributes.
 * In addEntityNamesAtEnd, we may have "AddEntities" or "AddAfter@" to signify what to add.
 * Here we (re)set addEntityNamesAtEnd and add hints appropriately.
 * @param [in] aname The name of the attribute or option, we assign the value to.
 * @param [in] avalue The value of the attribute or option, we assign.
 * @param [in] rewrite_source We rewrite the source of the hint to this.
 * @returns true, if we found good hints we set hint status to READY.
 *          False if we can check for other kinds of hints in the attr value.*/
bool BlockCsh::HandleBlockNamePlus(std::string_view aname, std::string_view avalue,
                                   EHintSourceType rewrite_source)
{
    if (hintStatus != HINT_FILLING) return false;
    hintSource = rewrite_source;
    if (hintSource==EHintSourceType::ATTR_VALUE)
        hintAttrName = aname;
    const int dir = AlignmentAttr::AttributeNameDir(aname);  //1 if a horizontal alignment attribute, 2 if vertical, 0 if else.
    const bool x = addEntityNamesAtEnd=="AddAfter@X";
    const bool y = addEntityNamesAtEnd=="AddAfter@Y";
    const bool add_num = addEntityNamesAtEnd=="AddEntities+num";
    if (add_num)
        addEntityNamesAtEnd = "AddEntities";
    if (addEntityNamesAtEnd=="AddEntities") {
        hintStatus = HINT_LOCATED; //So that additional hints can be added later, like attr values
        //OK, we just need to fill in the right description
        if (CaseInsensitiveEqual(aname, "generic_coord")) {
            addEntityNamesAtEnd = "Use one side of block %s to specify a coordinate.";
            hintStatus = HINT_READY; //Coordinates are not attributes, so nothing comes: finalize hints
        } else if (CaseInsensitiveEqual(aname, "via")) {
            if (avalue.length() && avalue.front()=='(')
                addEntityNamesAtEnd = "Add block %s to a waypoint specification this arrow must go through.";
            else
                addEntityNamesAtEnd = "Route around block %s (or a group of blocks containing it).";
        } else if (CaseInsensitiveEqual(aname, "size")) {
            addEntityNamesAtEnd = "Make width and height equal to block %s (or a group of blocks containing it).";
        } else if (CaseInsensitiveEqual(aname, "height")) {
            addEntityNamesAtEnd = "Make height equal to block %s (or a group of blocks containing it).";
        } else if (CaseInsensitiveEqual(aname, "width")) {
            addEntityNamesAtEnd = "Make width equal to block %s (or a group of blocks containing it).";
        } else if (CaseInsensitiveEqual(aname, "cross")) {
            addEntityNamesAtEnd = "Allow the arrow to cross block %s.";
        } else if (CaseInsensitiveEqual(aname, "cross_all")) {
            addEntityNamesAtEnd = "Allow the arrow to cross block %s and all its content.";
        } else if (CaseInsensitiveEqual(aname, "distance")) {
            addEntityNamesAtEnd = "Determine how far the arrow goes around block %s if it needs to.";
        } else if (dir) {
            addEntityNamesAtEnd = "Align to block %s (or a group of blocks containing it).";
        } else if (CaseInsensitiveEqual(aname, "modifier")) {
            addEntityNamesAtEnd = "Align to block %s (or a group of blocks containing it).";
        } else {
        instead:
            hintStatus = HINT_LOCATED; //So that additional hints can be added later
            addEntityNamesAtEnd.clear();
            return false;
        }
        if (add_num)
        //Add <number> as an option to the last element
            AddToHints(CshHint(HintPrefixNonSelectable() + "<number>",
                               "Shifts this many pixels down or right (up or left if negative).",
                               EHintType::ENTITY, false));
    } else if (addEntityNamesAtEnd=="AddAfter@" || x || y) {
        //Kill entities and add the pieces that may come after the @
        addEntityNamesAtEnd.clear();
        if (CaseInsensitiveEqual(aname, "generic_coord")) {
            if (x)
                AlignmentAttr::SideValuesAfterAt(*this, false, "To specify the x coordinate use ");
            else if (y)
                AlignmentAttr::SideValuesAfterAt(*this, true, "To specify the y coordinate use ");
            else {
                _ASSERT(0);
            }
        } else if (CaseInsensitiveEqual(aname, "via")) {
            if (avalue.length() && avalue.front()=='(') {
                _ASSERT(x||y);
                AlignmentAttr::SideValuesAfterAt(*this, y, {});
            } else
                for (auto p = ViaAttrSoloBlock::route_attr_values+2; **p; p += 2)
                    AddToHints(CshHint(HintPrefix(COLOR_ATTRVALUE) + (*p+8), std::string(p[1]).substr(0, strlen(p[1])-12) + ".",
                                       EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForVia, (p-ViaAttrSoloBlock::route_attr_values)/2));
        } else if (CaseInsensitiveEqual(aname, "distance")) {
            AddToHints(CshHint(HintPrefixNonSelectable() + "<pixel distance>",
                               "Specify how far the arrow should be from the block(s) listed.",
                               EHintType::ATTR_VALUE, false));
        } else if (CaseInsensitiveEqual(aname, "arrow")) {
            _ASSERT(x||y);
            AlignmentAttr::SideValuesAfterAt(*this, y, {});
        } else if (CaseInsensitiveEqual(aname, "size") ||
                   CaseInsensitiveEqual(aname, "height") ||
                   CaseInsensitiveEqual(aname, "width")) {
            AddToHints(CshHint(HintPrefixNonSelectable() + "<multiplier percent>",
                               "Specify a size multiplier, a positive percentage. If omitted, it defaults to one (meaning same size).",
                               EHintType::ATTR_VALUE, false));
        } else if (CaseInsensitiveEqual(aname.substr(1), "pos")) { //either xpos or ypos
            //explicitly nothing after @ for these
            return true;
        } else if (dir==1) {
            AlignmentAttr::SideValuesAfterAt(*this, false, "Align horizontally to ");
        } else if (dir==2) {
            AlignmentAttr::SideValuesAfterAt(*this, true, "Align vertically to ");
        } else
            goto instead;
        //So that no additional hints can be added later.
        //Here we are after a @ and we do not want to add hints for the whole attribute value
        hintStatus = HINT_READY;
    } else
        goto instead;
    return true;
}

/** This is called when a block header is completed in the input file.
 * Used to add a csh entry, register the new name and adjust 'parent_name'.
 * Returns false, if there is already such a block. (Coloring is then underlined.)
 * If pname is empty we do not add an actual name (and return true), but
 * we do set the 'parent_name' correctly.
 * If the block of this name exists, we set its shape to -2.*/
bool BlockCsh::AddCSH_NewBlock(const CshPos &pos, std::string_view pname, int shape)
{
    if (pname.empty()) {
        parent_name = GetCurrentPrefix();
        if (!pos.empty())
            AddCSH(pos, COLOR_ENTITYNAME); //probably pname = "" (two double quotes)
        return true;
    }
    if (shape<0) shape = -1; //box shape
    bool ret = true;
    parent_name = PrefixName(pname);
    if (!Contexts.back().SkipContent()) {
        auto [itr, inserted] = Blocks.insert(std::pair<std::string, int>(parent_name, shape));
        if (!inserted)
            itr->second = -2;
        else if (CursorIn(pos)>CURSOR_AFTER)
            //We are inside this particular name block.
            //exclude from providing hints if not yet defined
            exclude_entity_hint = parent_name;
        ret = !inserted;
    }
    BlockMentions.emplace_back(pos, parent_name);
    if (ret)
        AddCSH(pos, COLOR_ENTITYNAME);
    else
        AddCSH(pos, COLOR_ENTITYNAME_FIRST);
    return ret;
}

/** Finds an entity.
 * @param [in] name The name of the entity as typed by the user. We search namespaces and apply
 *             our current prefix. For unqualified names (no dot), we also do magic lookup.
 * @param [in] may_be_new This is a place where we may either reference an existing entity but may
 *             also define a new entity. If the name contains no dots (magic lookup applies) and
 *             it does not exist in the current namespace (only magic lookup could yield a hit),
 *             but it appears multiple times in other namespaces (so that magic lookup would
 *             yield an ambiguous result), we do not return -3, but -4, to allow defining a new
 *             entity.
 * @Returns
 * - >=-1 if this is a valid entity name from the current context (the shape of the block)
 * - -2 if this is the prefix of a partial name.
 * - -3 if this is a matching name, but multiple blocks match and it is ambiguous.
 * - -4 if this is not a valid name (or not found).
 *
 * Also returns the full name of the block, if we don't skip content & found a valid unambiguous match.*/
std::pair<int, std::string_view> BlockCsh::SearchEntityName(std::string_view string, bool may_be_new) const {
    const auto r = SearchName(Blocks, string, may_be_new, true);
    switch (r.first) {
    case -1: //found
        if (r.second == Blocks.end()) return { -1, {} };
        return { r.second->second < -1 ? -3 : r.second->second, r.second->first };
    case -2:
    case -3:
        return { r.second->second, r.second->first };
    default:
        return { -4, {} };
    }
}

void BlockCsh::AddEntityNamesAtTheEnd(std::string_view msg)
{
    addEntityNamesAtEnd = msg;
    prefix_at_hint_loc = GetCurrentPrefix();
    prefix_at_hint_loc.push_back('.'); //always end in a dot even if empty
}

/** Returns true if 'name' equals 'prefix' or starts with 'prefix' followed by a dot.*/
bool dot_hierarchy_starts_with(std::string_view name, std::string_view prefix) {
    if (prefix.empty()) return true;
    if (name.length()<prefix.length()) return false;
    if (name.substr(0, prefix.length())!=prefix) return false;
    if (name.length()==prefix.length()) return true;
    return name[prefix.length()]=='.';
}

/** Returns true if 'name' equals 'suffix' or ends with a dot followed by 'suffix'.*/
bool dot_hierarchy_ends_with(std::string_view name, std::string_view suffix) {
    if (suffix.empty()) return true;
    if (name.length()<suffix.length()) return false;
    if (name.substr(name.length()-suffix.length())!=suffix) return false;
    if (name.length()==suffix.length()) return true;
    return name[name.length()-suffix.length()-1]=='.';
}

/** Returns the number of chars before the first dot in 'a' and 'b' before which 'a' and 'b' are the same.
 * If 'a' and 'b' does not have such a common prefix, we return zero. If a==b we return their length.
 * If one of them is a prefix of the other, we return the length of that.
 * So
 * - a.b, a.c.d => 1
 * - a.b, a.b => 3
 * - a.b, b.b => 0
 * - a.b, a.b.c => 3*/
unsigned dot_hierarchy_common_prefix(std::string_view a, std::string_view b) {
    unsigned ret = 0, i;
    for (i = 0; i<std::min(a.length(), b.length()); i++)
        if (a[i]!=b[i]) return ret;
        else if (a[i]=='.') ret = i;
    return i;
}


void BlockCsh::AfterYaccParse()
{
    _ASSERT(!CaseInsensitiveBeginsWith(addEntityNamesAtEnd, "AddEntities"));
    _ASSERT(!CaseInsensitiveBeginsWith(addEntityNamesAtEnd, "AddAfter@"));
    if (CaseInsensitiveBeginsWith(addEntityNamesAtEnd, "AddAfter@")||
        CaseInsensitiveBeginsWith(addEntityNamesAtEnd, "AddEntities"))
        //TODO: This happens when the parser gets an error and parsing breaks or something.
        //Temp fix until I hunt it down.
        addEntityNamesAtEnd = "Use block %s.";
    _ASSERT(EntityNames.size()==0);
    ///Here we add the Block names if needed as seen from prefix 'prefix_at_hint_loc'
    if (addEntityNamesAtEnd.length()) {
        hintStatus = HINT_FILLING;
        for (auto n = Blocks.begin(); n!=Blocks.end(); n++) {
            std::string desc = addEntityNamesAtEnd;
            //Omit the common prefix length
            size_t plen = CaseInsensitiveCommonPrefixLen(n->first, prefix_at_hint_loc);
            if (plen) {
                //limit common prefix to a dot
                plen = std::string_view(n->first.data(), plen).find_last_of('.');
                if (plen == std::string_view::npos)
                    plen = 0;
                else plen++;
            }
            //Now plen contains the common part of the block name (n.first) and
            //the prefix including the dot
            auto at = desc.find("%s");
            if (at!=std::string::npos)
                desc.replace(at, 2, n->first.substr(plen));
            //Skip adding this block if its name is under definition
            if (n->first != exclude_entity_hint)
                AddToHints(CshHint(HintPrefix(COLOR_ENTITYNAME) + n->first.substr(plen),
                                   std::move(desc), EHintType::ATTR_VALUE, true));
            //Now check if we have children. If we are a prefix of the block name following us,
            //we have children.
            auto next = std::next(n);
            if (next!=Blocks.end() && next->first.length()>n->first.length() &&
                next->first[n->first.length()]=='.' &&
                strncmp(n->first.data(), next->first.data(), n->first.length())==0) {
                //Add an A.B.C.* hint
                desc = addEntityNamesAtEnd;
                if (at!=std::string::npos)
                    desc.replace(at, 2, n->first.substr(plen)+".*");
                AddToHints(CshHint(HintPrefix(COLOR_ENTITYNAME) + n->first.substr(plen)+".*",
                                   std::move(desc), EHintType::ATTR_VALUE, true));
            }
        }
        hintStatus = HINT_READY;
        addEntityNamesAtEnd.clear();
    }
    Csh::AfterYaccParse();
    //Resolve Block forward references
    for (auto &bm : BlockMentions)
        if (bm.prefix) {
            if (bm.name.starts_with('$')) {
                AddCSH(bm.pos, COLOR_PARAMNAME);
                bm.name.clear(); //remove this mention - only a param name.
            } else {
                std::string_view match; //The full name of the entity we match
                bool precise_match = false; //True if the match was fully specified and not something we added a prefix for
                bool ambiguous = false;
                //bm.name contains the suffix of the full name, bm.prefix contains the prefix we referenced the name from
                //E.g., if we want to reference a.b.c.d and used bm.name=c.d from bm.prefix=a.b.c, we shall match.
                //We also match anyone in a 'deeper' namespace than the point of reference, but then only tentatively
                //E.g., bm.prefix='a', bm.name='c' will also match a.b.c, but only if we don't have 'c' or 'a.c'
                for (const auto& [bname, btype] : Blocks)
                    if (dot_hierarchy_ends_with(bname, bm.name)) {
                        //When testing bname='a.b.c.d' we get here for
                        // 1. bm.prefix='', bm.name='a.b.c.d' (good, this is a match)
                        // 2. bm.prefix='', bm.name='b.c.d' (bad, this is not a match)
                        // 3. bm.prefix='a.b', bm.name='c.d' (good)
                        // 4. bm.prefix='a.b', bm.name='b.c.d' (good)
                        // 5. bm.prefix='a.b', bm.name='d' (bad)
                        // 6. bm.prefix='hulu', bm.name='a.b.c.d' (good)
                        // 7. bm.prefix='a.hulu', bm.name='b.c.d' (good)
                        //So we first find the common (fully dot separated) part of bm.prefix and bname
                        const std::string_view common_prefix{bname.data(), dot_hierarchy_common_prefix(bname, *bm.prefix)};
                        //Then we test if the combined length of this prefix and bm.name  is longer than bname.
                        //If the common prefix is '', we just test if bm.name is as long as bname.
                        //Else we add 1 to cater for case #3 above.
                        if ((common_prefix.empty() && bname == bm.name)
                            || (common_prefix.size() + 1 + bm.name.size() >= bname.size())) {
                        //OK, this is a match.
                        //If a previous match is a suffix of us (or empty), we take precedence
                        //Else we have ambiguity.
                        //Thus if we have bm.name="c" bm.prefix="b"
                        //Then we match both b.c and c, but prefer b.c
                            if (!precise_match || dot_hierarchy_ends_with(bname, match)) {
                                match = bname;
                                precise_match = true;
                                ambiguous = false;
                            }
                        } else if (!precise_match) {
                            if (match.empty()) match = bname;
                            else ambiguous = true;
                        }
                    }
                if (match.empty()) {
                    if (hadProcReplay) //we have replayed a procedure - blocks not found can be quite valid.
                        AddCSH(bm.pos, COLOR_ENTITYNAME); //just color it ENTITY, but clear from mentions (no rename)
                    else
                        AddCSH_Error(bm.pos, "Block name not found.");
                    bm.name.clear();
                } else if (ambiguous) {
                    AddCSH_Error(bm.pos, "Block name ambiguous (compile to get candidates).");
                    bm.name.clear();
                } else {
                    bm.name = match;
                    bm.prefix.reset();
                    AddCSH(bm.pos, COLOR_ENTITYNAME);
                }
            }
        }
    std::erase_if(BlockMentions, [](const BlockMention& bm) {return bm.name.empty(); });
    Blocks.erase("prev");
    Blocks.erase("next");
    Blocks.erase("first");
    Blocks.erase("last");
}

void BlockCsh::UpdateBlocks(const CshStringViewWithPosList *l) {
    if (!l) return;
    for (const CshStringViewWithPos &sp : *l) {
        auto [i, full_name] = SearchEntityName(sp.name, false);
        if (i>=-1)
            BlockMentions.emplace_back(sp.file_pos, full_name);
    }
}


/** Colors a solo string token at the line begin.
 * Lines can start with
 * - keywords: these are recognized by the parser and appear here only partial.
 * - chart options
 * - element attribute names
 * - entities
 * - or the partial version of any of these.*/
void BlockCsh::AddCSH_LineBeginSoloString(CshPos pos, std::string_view string) {
    //First see if we have a good block name
    //Here we only search for already established block names - and cannot redefine them

    //option_names already contain chart options and element attribute names
    const unsigned opt = FindPrefix(option_names, string);
    const auto [ent, full_name] = SearchEntityName(string, !GetPedantic());
    const unsigned key = FindPrefix(keyword_names, string);
    const auto color = [&]() {
        if (opt==2) return COLOR_OPTIONNAME;
        if (key==2) return COLOR_KEYWORD;
        if (ent>=-1) return COLOR_ENTITYNAME; //good block name
        if (key==1) { was_partial = true; return COLOR_KEYWORD_PARTIAL; }
        if (opt==1) { was_partial = true; return COLOR_OPTIONNAME_PARTIAL; }
        if (ent==-2) { was_partial = true; return COLOR_ENTITYNAME_PARTIAL; }
        return COLOR_ERROR;
    }();
    if (color==COLOR_ERROR)
        AddCSH_Error(pos, "Unrecognized keyword, block name, option or attribute name.");
    else {
        if (full_name.size())
            BlockMentions.emplace_back(pos, full_name);
        AddCSH(pos, color);
    }
}

void BlockCsh::AddCSH_AfterMultiPartial(CshPos pos, std::string_view string)
{
    if (CaseInsensitiveBeginsWith("boxcol", string))
        AddCSH(pos, COLOR_KEYWORD_PARTIAL);
}


/** This only gets called when we refer to an entity and not when defined.
 * (For that use AddCSH_NewBlock().)
 * if cinstead is not COLOR_ERROR, then any text NOT a fully valid blockname, will be colored so
 * and no error is given.
 * Returns the same as SearchEntityName().
 * Works incorrectly for blocks defined in a procedure.*/
int BlockCsh::AddCSH_BlockName(const CshPos&pos, std::string_view name, EColorSyntaxType cinstead)
{
    const auto [ret, full_name] = SearchEntityName(name, false);
    if (ret>=-1) {
        AddCSH(pos, COLOR_ENTITYNAME);
        if (full_name.size())
            BlockMentions.emplace_back(pos, full_name);
        return ret;
    }
    if (cinstead==COLOR_ERROR)
        AddCSH_BlockNameError(pos, name, ret);
    else
        AddCSH(pos, cinstead);
    return ret;
}

/** Colors a block name, that must be from the local parent.
 * It is used inside copy blocks when looking up the element to manipulate
 * (drop X, update X, replace X) or after 'before' clause. */
void BlockCsh::AddCSH_LocalBlockName(const CshPos& pos, std::string_view name) {
    if (name.empty()) return;
    std::string n = PrefixName(name);
    auto i = Blocks.lower_bound(n);
    if (i==Blocks.end()) return;
    if (i->first==n) {
        AddCSH(pos, COLOR_ENTITYNAME);
        BlockMentions.emplace_back(pos, n);
    } else if (i->first.starts_with(n))
        AddCSH(pos, COLOR_ENTITYNAME_PARTIAL);
    else
        AddCSH_BlockNameError(pos, name, -4); //not found
}


/** This only gets called when we refer to an entity in an arrow or update command.
 * It behaves like AddCSH_BlockName() if pedantic is on.
 * Else it behaves as AddCSH_NewBlock() with the current default shape.
 * Works incorrectly for blocks defined in a procedure.*/
int BlockCsh::AddCSH_BlockNameOrNew(const CshPos&pos, std::string_view name)
{
    const auto [ret, full_name] = SearchEntityName(name, true);
    if (ret>=-1) {
        AddCSH(pos, COLOR_ENTITYNAME);
        if (full_name.size())
            BlockMentions.emplace_back(pos, full_name);
        return ret;
    }
    if (ret!=-3 && !GetPedantic() && std::string::npos == name.find('.')) {
        const int shape = Context2.empty() ? -1 : Context2.back().def_shape;
        AddCSH_NewBlock(pos, name, shape);
        return shape;
    }
    AddCSH_BlockNameError(pos, name, ret);
    return ret;
}

/** Adds a CSH error for a block name, when not found or ambiguous.
 * err can be
 * - >=-1: No error is added, this turned up to be a valid name.
 * - -2: This is a partial name, we use COLOR_ENTITYNAME_PARTIAL.
 * - -3: This name is ambiguous, we give an error
 * - -4: This name is not found, we give an error*/

void BlockCsh::AddCSH_BlockNameError(const CshPos& pos, std::string_view /*name*/, int err) {
    switch (err) {
    default: return;
    case -2: AddCSH(pos, COLOR_ENTITYNAME_PARTIAL); return;
    case -3: AddCSH_Error(pos, "This name matches multiple blocks."); return;
    case -4:
        if (hadProcReplay) AddCSH(pos, COLOR_ENTITYNAME);
        else AddCSH_Error(pos, "Block not found.");
    }
}

/** This is called when we realized that an arrow_end is standing at the beginning of a line.
 * At this point we have already colored it.
 * If this is a simple string, then it may also be a partial or full keyword
 * and its color is a new block or an error depending on whether we have pedantic
 * off or on.
 * In this functions we adjust the coloring of the arrow_end if it is a single string.
 * If the string is a compound string (a coordinate or \<a>+\<b>, etc), we do nothing. */
void BlockCsh::UpdateCSH_ArrowEndAtLineBegin(const CshPos& pos, std::string_view arrow_end) {
    if (arrow_end.find_first_of("@,()+-")!=std::string_view::npos)
        return;
    if (CshList.empty() && CshErrors.error_ranges.empty()) {
        AddCSH_LineBeginSoloString(pos, arrow_end);
        return;
    }
    //If the previous CSH entry at this pos is this arrow_end, but it is not a block mention
    //(which may happen if pedantic is on and this is not an already existing block)
    //then we re-color it as a line begin string (keywords, options, existing blocks, etc.)
    if (const auto i = std::ranges::find(CshList, pos); i!=CshList.end()) {
        if (std::ranges::find(BlockMentions, pos, &BlockMention::pos)==BlockMentions.end()) {
            CshList.erase(i);
            AddCSH_LineBeginSoloString(pos, arrow_end);
        } //else it is a block mention and we keep its color
    } else if (const auto i = std::ranges::find(CshErrors.error_ranges, pos); i!=CshErrors.error_ranges.end()) {
        const unsigned opt = FindPrefix(option_names, arrow_end);
        const auto [ent, full_name] = SearchEntityName(arrow_end, !GetPedantic());
        const unsigned key = FindPrefix(keyword_names, arrow_end);
        std::string_view msg;
        const auto color = [&]() {
            if (opt==2) return COLOR_OPTIONNAME;
            if (key==2) return COLOR_KEYWORD;
            if (ent>=-1) return COLOR_ENTITYNAME; //good block name
            if (key==1) { was_partial = true; i->text.append(" Only a partial keyword.");  return COLOR_KEYWORD_PARTIAL; }
            if (opt==1) { was_partial = true; i->text.append(" Only a partial option name."); return COLOR_OPTIONNAME_PARTIAL; }
            if (ent==-2) { was_partial = true; i->text.append(" Only a partial entity name."); return COLOR_ENTITYNAME_PARTIAL; }
            i->text.append(" Not a keyword or option.");
            return COLOR_ERROR;
        }();
        if (color!=COLOR_ERROR)
            AddCSH(pos, color);
    }
}

/** Marks a part of the text as a block name and registers it among
 * BlockMentions. Called for block names as attribute values.
 * We try to look up the Block among already defined
 * blocks, but forward references are also allowed.*/
void BlockCsh::AddCSH_BlockNameAsAttrValue(const CshPos &pos, std::string_view name) {
    //register a partial match
    BlockMentions.emplace_back(pos, name, GetCurrentPrefix());
}

void BlockCsh::AddCSH_AttrValue_CheckAndAddEscapeHint(const CshPos& pos, std::string_view value, std::string_view name)
{
    //for alignment attributes it is only called if there is no + sign or @ symbol
    if (AlignmentAttr::AttributeNameDir(name)) return;
    //These attributes take block names
    static const char* anames[] = {"via", "cross", "cross_all", "distance", "height", "Width", "size"};
    if (std::ranges::any_of(anames, [name](const char* n) {return CaseInsensitiveEqual(name, n); })) return;
    //Remove potentially added coloring
    RemoveAllTouchingCSH(pos);
    //remove anything potentially added hints
    if (pos.IsWithin(cursor_pos))
        addEntityNamesAtEnd.clear();
    Csh::AddCSH_AttrValue_CheckAndAddEscapeHint(pos, value, name);
    DropBlockMentions(pos);
}

void BlockCsh::DropBlockMentions(CshPos pos) {
    while (BlockMentions.size())
        if (pos.IsWithin(BlockMentions.back().pos))
            BlockMentions.pop_back();
        else
            break;
}

/** Take a CshStringWithPosList and color according to it.
 * We may assume the elements come in order.
 * @param [in] l The list contains tokens (names) and their positions.
 *               Names may be empty, in this case an error occurred there and
 *               we don't need to color.
 * @param [in] pos The whole span we color. We shall also color before and after
 *                 any blocks within this position range.
 * @param [in] cname Color names to this color (except empty ones)
 *                   If COLOR_ENTITYNAME, we color the strings as block names
 *                   and add errors if malformed.
 * @param [in] cbetween Color the text between blocks to this (comma or + signs)
 * @param [in] cnumber if not COLOR_ERROR, then color any name that parses a valid
 *             number like this instead of cname.
 * @param [in] cinstead if not COLOR_ERROR and cname is COLOR_ENTITYNAME and the
 *                      list is only of one element and that is not a blockname,
 *                      we use this color instead.*/
void BlockCsh::AddCSH_NameList(const CshStringViewWithPosList* l, const CshPos& pos,
                               EColorSyntaxType cname, EColorSyntaxType cbetween,
                               EColorSyntaxType cnumber, EColorSyntaxType cinstead)
{
    if (l==nullptr) return;
    CharByteIndex after_prev_last = pos.first_pos;
    double dummy;
    for (auto &s : *l) {
        if (after_prev_last<s.file_pos.first_pos)
            AddCSH(CshPos(after_prev_last, s.file_pos.first_pos), cbetween);
        if (cnumber!=COLOR_ERROR && !from_chars(s.name, dummy))
            AddCSH(s.file_pos, cnumber);
        else if (cname!=COLOR_ENTITYNAME)
            AddCSH(s.file_pos, cname);
        else if (cinstead!=COLOR_ENTITYNAME)
            AddCSH_BlockName(s.file_pos, s.name,
                             l->size()==1 ? cinstead : COLOR_ERROR);
        else
            AddCSH_BlockNameAsAttrValue(s.file_pos, s.name);
        after_prev_last = s.file_pos.end_pos;
    }
    if (after_prev_last<pos.end_pos)
        AddCSH(CshPos(after_prev_last, pos.end_pos), cbetween);
}

Csh::EntityNameUnderResult BlockCsh::EntityNameUnder(long pos) const {
    if (auto res = EntityNameUnderInLined(pos)) return res;
    auto i = std::find_if(BlockMentions.begin(), BlockMentions.end(),
                          [pos](const BlockMention &p) { return p.pos.IsWithinChar(pos); });
    if (i==BlockMentions.end())
        return {};
    EntityNameUnderResult res = Csh::EntityNameUnder(pos);
    //the full block name must end with what is in the text file.
    const int prefix_len = int(i->name.length())-int(res.full_entity_name.length());
    if (prefix_len<0 || std::string_view(i->name).substr(prefix_len) != res.full_entity_name) {
        _ASSERT(0);
        return {};
    }
    //1. Trim away parts that are not clicked on by the user. E.g., For A.B.C if the user
    // clicks on 'B', we just need to return A.B (even if in the text we only have 'B.C')
    const auto bytes = GetUTF8ByteIndex(i->name, pos-i->pos.first_pos.char_index);
    if (bytes<0) return {};
    const size_t dot_after = i->name.find('.', bytes);
    const size_t limit_len_to = dot_after == string::npos ? i->name.length() : bytes+dot_after;
    //2. Adjust the click pos by adding the num of chars in the prefix not written in the
    //source file to it (If the source file has B.C and we clicked 'C' and the full name
    // of the entity is A.B.C then pos_in_src_in_entity==2 and we now add len('A.') to it.
    return { .full_entity_name = std::string_view(i->name).substr(0, prefix_len + limit_len_to),
            .byte_pos = res.byte_pos + (int)UTF8len(std::string_view(i->name).substr(0, prefix_len)),
            .csh = this };
}

std::string_view BlockCsh::AskReplace(std::string_view full_entity_name, int byte_pos) const {
    //Take the dot-separated segment 'byte_pos' (as a byte index) is in.
    //If 'byte_pos' is on a dot, we take what is before.
    // E.g., In aaa.bbb.ccc, we take 'aaa' if pos=[0..3], 'bbb' if pos==[4..7], etc.
    byte_pos = std::max(0, byte_pos);
    size_t from = full_entity_name.substr(0, byte_pos).find_last_of('.'); //works even with UTF-8.
    if (from==std::string_view::npos) from = 0;
    else from++;
    size_t till = full_entity_name.substr(byte_pos).find_first_of('.');
    if (till==std::string_view::npos) till = full_entity_name.length();
    else till += byte_pos;
    return full_entity_name.substr(from, till-from);
}
std::string BlockCsh::ReplaceEntityName(std::string_view full_entity_name, int byte_pos,
                                        std::string_view replace_to,
                                        long lStart, long lEnd) const {
    //Trim block names after the element to replace
    size_t till = full_entity_name.substr(byte_pos).find_first_of('.');
    if (till==std::string_view::npos) till = full_entity_name.length();
    else till += byte_pos;
    full_entity_name = full_entity_name.substr(0, till);
    std::string text{input_text};
    if (lStart == lEnd) {
        lStart = first_pos.char_index;
        lEnd = INT_MAX;  //We do not care about end_pos here, just something very large
    }
    //The positions in 'BlockMentions' are in csh units: the first char is
    //indexed 1
    //Collect potential places to replace, where the CSH range
    //- is within the selection
    //- denotes the block we replace (or a child of it)
    std::vector<std::pair<CshPos, std::string_view>> poses; 
    for (const auto &[pos, fname, prefix] : BlockMentions)
        if (std::string_view(fname).substr(0, full_entity_name.length()) == full_entity_name
            && lStart <= pos.first_pos.char_index && pos.end_pos.char_index <= lEnd) {
            _ASSERT(first_pos.byte_index <= pos.first_pos.byte_index);
            _ASSERT(pos.end_pos.byte_index <= first_pos.byte_index + (int)input_text.length());
            poses.emplace_back(pos, fname);
        }
    if (poses.empty()) return text;
    std::ranges::reverse(poses); //helps reverse sort
    std::ranges::sort(poses, std::greater<>()); //sort reverse
    //Find the position of the part to replace in 'full_entity'
    //That is, if we have A.B.C and replace to 'D', we find 'C'.
    const size_t last_dot = full_entity_name.find_last_of('.'); //works with UTF8
    const size_t replace_pos_in_full_name = last_dot==std::string::npos ? size_t(0) : last_dot+1;
    const size_t replace_len_in_full_name = full_entity_name.length()-replace_pos_in_full_name;
    //Walk the input UTF-8 text to find character positions
    for (const auto &[pos, fname] : poses) {
        std::string_view src_name = std::string_view(input_text).substr(pos.first_pos.byte_index-first_pos.byte_index, pos.byte_length());
        //Locate what part of the text to replace: block name in source (src_name) may be a partial one
        //That is, if we replace A.B.C to D (becoming A.C.D in the process),
        //We may have in the source file (src_name may be):
        // - A.B.C: easy just replace 'C'->'D'
        // - B.C (but we are sure it denotes A.B.C): again easy
        // - A.B.C.X or B.C.X, where we know it refers to A.B.C.X: here we need to find 'C'
        // - X (where we know it refers to A.B.C.X): nothing to replace here.
        //What is certain fname ends with src_name - and start with full_name (we filtered BlockMentions above).
        //Find what position the source text
        _ASSERT(fname.length()>=src_name.length() && fname.substr(fname.length()-src_name.length())==src_name);
        const ptrdiff_t start_at = ptrdiff_t(replace_pos_in_full_name) - ptrdiff_t(fname.length()-src_name.length());
        if (0<=start_at)
            text.replace(pos.first_pos.byte_index+start_at, replace_len_in_full_name, replace_to);
    }
    return text;
}
