/*
    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 cgen_attribute.cpp The implementation of Attribute and line, fill, shadow and width attributes.
* @ingroup libcgencommon_files */

#include <cassert>
#include <cstring>
#include "cgen_attribute.h"
#include "chartbase.h"
#include "stringparse.h"
#include "utf8utils.h"

unsigned CaseInsensitiveCommonPrefixLen(std::string_view a, std::string_view b) noexcept
{
    unsigned i=0;
    while(i<a.length() && i<b.length()) {
        if (tolower(static_cast<unsigned char>(a[i])) != tolower(static_cast<unsigned char>(b[i])))
            return i;
        i++;
    }
    return i;
}

int CaseInsensitiveBeginsWith(std::string_view a, std::string_view b) noexcept
{
    unsigned i=CaseInsensitiveCommonPrefixLen(a, b);
    if (i==b.length()) return i==a.length()?2:1;
    return 0;
}

bool CaseInsensitiveEndsWith(std::string_view base, std::string_view a) noexcept
{
    int i1 = int(a.length())-1;
    int i2 = int(base.length())-1;
    if (i1>i2 || i1<0 || i2<0) return false;
    while(i1>=0 && i2>=0) {
        if (toupper(static_cast<unsigned char>(a[i1])) != toupper(static_cast<unsigned char>(base[i2])))
            return false;
        i1--; i2--;
    }
    //Here the end of `base` equals to `a`.
    //If i2<0 then `a` must equal to `base`, which is a true return.
    //Else, we return true only if `a` comes in `base` strictly after a dot.
    return i2<0 || base[i2]=='.';
}

bool CaseInsensitiveSmaller(std::string_view a, std::string_view b) noexcept {
    size_t j;
    for (j = 0; j < a.length() && j < b.length(); j++)
        if (toupper(a[j]) != toupper(b[j]))
            return toupper(a[j]) < toupper(b[j]);
    //We have reached the end of one of the strings
    if (j == a.length())
        return  j < b.length() || a > b; //if equal case insensitive, return case sensitive, but make 'Table' later than 'table'. Note 'Table' < 'table' in ASCII.
    return false; //j==b.length() must hold
}

std::optional<size_t> CaseInsensitiveContains(std::string_view base, std::string_view a) noexcept {
    if (base.length() < a.length()) return {};
    for (size_t i = 0; i<=base.length()-a.length(); i++)
        if (CaseInsensitiveCommonPrefixLen(base.substr(i), a)==a.length())
            return i;
    return {};
}


Attribute::Attribute(std::string_view a, std::string_view s, const FileLineColRange & l, const FileLineColRange & v,
                     bool force_string) :
    type(EAttrType::STRING), name(a), value(s),
    linenum_attr(l), linenum_value(v)

{
    if (force_string) return;
    if (s.length()==0) {
        type = EAttrType::CLEAR;
    } else if (CaseInsensitiveEqual(s, "no") || CaseInsensitiveEqual(s, "false")) {
        type = EAttrType::BOOL;
        yes = false;
    } else if (CaseInsensitiveEqual(s, "yes") || CaseInsensitiveEqual(s, "true")) {
        type = EAttrType::BOOL;
        yes = true;
    } else if (!from_chars(s, number))
        type = EAttrType::NUMBER;
}

/** Creates a copy of an attribute pushing all locations (even in values).*/
Attribute::Attribute(const Attribute & o, const FileLineCol & l, EInclusionReason r) :
    type(o.type),
    name(StringFormat::PushPosEscapes(o.name, l, r)),
    value(StringFormat::PushPosEscapes(o.value, l, r)),
    number(o.number),
    yes(o.yes),
    linenum_attr(o.linenum_attr),
    linenum_value(o.linenum_value),
    error(o.error)
{
    linenum_attr.Push(l, r);
    linenum_value.Push(l, r);
}

/** Creates a copy of an attribute pushing all locations (even in values).*/
Attribute::Attribute(const Attribute & o, const FileLineColRange & l, EInclusionReason r) :
    type(o.type),
    name(StringFormat::PushPosEscapes(o.name, l.start, r)),
    value(StringFormat::PushPosEscapes(o.value, l.start, r)),
    number(o.number),
    yes(o.yes),
    linenum_attr(o.linenum_attr),
    linenum_value(o.linenum_value),
    error(o.error)
{
    linenum_attr.Push(l, r);
    linenum_value.Push(l, r);
}

/** Converts the attribute to a string
 * @param [in] indent Tells how much space to prepend each line.
 * @returns The string */
string Attribute::Print(int indent) const
{
    string s(indent*2,' ');
    s << name ;
    switch (type) {
    case EAttrType::STYLE:
        s << " (style)";
        break;
    case EAttrType::CLEAR:
        s << "=";
        break;
    case EAttrType::STRING:
        s << "=" << value;
        break;
    case EAttrType::NUMBER:
        s << "=" << number;
        break;
    case EAttrType::BOOL:
        s << "=" << (yes?"yes":"no");
        break;
    };
    return s;
};

/** True if the (one or more) last (dot-separated) segments of the attribute name equal `a` (case insensitive)*/
bool Attribute::EndsWith(std::string_view a) const noexcept
{
    return CaseInsensitiveEndsWith(name.c_str(), a);
}

/** True if the (one or more) first (dot-separated) segments of the attribute name equal `a` (case insensitive)*/
bool Attribute::StartsWith(std::string_view a) const noexcept
{
    unsigned i=0;
    while(i<a.length() && i<name.length()) {
        if (toupper(static_cast<unsigned char>(a[i])) != toupper(static_cast<unsigned char>(name[i])))
            return false;
        i++;
    }
    return i==a.length() && (i==name.length() || name[i]=='.');
}

/** Ensure that the attribute is of a certain type.
 *
 * If the attribute is not of type `t` we generate an error into `error` and
 * mark the attribute as one in error (which will result in ignoring it).
 * @param [in] t The type the attribute shall be.
 * @param error The object we shall add our error message to.
 * @returns True if the types matched.*/
bool Attribute::CheckType(EAttrType t, MscError &error) const
{
    if (type == t) return true;
    string ss;
    ss << "Value for '" << name << "' ";
    switch (t) {
    case EAttrType::STYLE:
        error.Error(*this, true, "Expecting a style here instead of an attribute.");
        break;
    case EAttrType::CLEAR:
        error.Error(*this, true, "I expect no value for attribute '" + name + "'.");
        break;
    case EAttrType::STRING:
        if (type != EAttrType::STYLE)
            return true;
        ss << "Attribute '" << name << "' seems to be a style. Ignoring attribute.";
        error.Error(*this, true, ss, "Use quotation marks to force this value.");
        break;

    case EAttrType::BOOL:
        error.Error(*this, true, ss << "must be 'yes' or 'no'. Ignoring attribute.");
        break;

    case EAttrType::NUMBER:
        error.Error(*this, true, ss << "must be a number. Ignoring attribute.");
        break;
    }
    return false;
}

/** Marks the attribute as one having an invalid value and generates an appropriate error message.
 *
 * @param [in] candidates The slash ("/") separated list of value candidates that could be used instead of the faulty value.
 * @param error The object we shall add the error message to.*/
void Attribute::InvalidValueError(std::string_view candidates, MscError &error) const
{
    if (type == EAttrType::STYLE)
        return error.Error(*this, true, StrCat("Missing '=' sign after attribute '", name, "'."), StrCat("Do not use '", name, "' as a style name.'"));
    string s = string("Invalid value '");
    switch (type) {
    case EAttrType::STRING: s << StringFormat::RemovePosEscapesCopy(value.c_str()); break; //In Block diagrams, block groups are prefixed with location escapes. Do not leak to error messages
    case EAttrType::BOOL:   s << (yes?"yes":"no"); break;
    case EAttrType::NUMBER: s << number; break;
    case EAttrType::CLEAR: break; //complain about empty value
    case EAttrType::STYLE:
    default:
        _ASSERT(0);
    }
    s.append("' for attribute '").append(name);
    s.append("'. Use '").append(candidates).append("'. Ignoring attribute.");
    error.Error(*this, true, s);
}

/** Marks the attribute as not applicable here and generates an appropriate error message.
 *
 * @param error The object we shall add the error message to.*/
void Attribute::InvalidAttrError(MscError &error) const
{
    string s;
    s << "Attribute  '" << name << "' cannot be applied here. Ignoring it.";
    error.Error(*this, false, s);
}

/** Marks the attribute an unknown style and generates an appropriate error message.
 *
 * We assume the attribute specifies a style (is of EAttrType::STYLE type).
 * @param error The object we shall add the error message to.*/
void Attribute::InvalidStyleError(MscError &error) const
{
    _ASSERT(type==EAttrType::STYLE);
    string s;
    s << "Undefined style: '" << StringFormat::RemovePosEscapesCopy(name.c_str()) << "'. Ignoring it.";
    error.Error(*this, false, s);
}

/** Ensures the attribute has a value after the "=" sign.
 *
 * We generate an error message if not so.
 * Some attributes cannot be unset (or cleared), we check this here.
 * @param error The object we shall add the error message to.
 * @param [in] t The type of style the attribute is part of.
 * @returns true if the attribute is indeed not clear.*/
bool Attribute::EnsureNotClear(MscError &error, EStyleType t) const
{
    if (type != EAttrType::CLEAR) return true;
    string s;
    switch (t) {
    case EStyleType::STYLE:
    case EStyleType::DEF_ADD:
        return true;
    case EStyleType::ELEMENT:
        //loss attributes are delta, so they may be unset //TODO: refactor this out from here to [signalling]
        if (StartsWith("lost")) return true;
        s << "Can not unset element attribute '" << name << "'.";
        break;
    case EStyleType::DEFAULT:
        //text and loss attributes are delta, so they may be unset
        if (StartsWith("text") || StartsWith("lost")) return true;
        s << "Can not unset attribute '" << name << "' of a default style.";
        break;
    case EStyleType::OPTION:
        s << "Can not unset chart option '" << name << "'.";
        break;
    }
    s.append(" Ignoring attempt.");
    error.Error(*this, false, s, "Can only unset attributes of non-default styles.");
    return false;
}


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

/** Possible values for a line type*/
template<> const char EnumEncapsulator<ELineType>::names[][ENUM_STRING_LEN] =
    {"invalid", "none", "solid", "dotted", "dashed", "long_dashed", "dash_dotted",
     "double", "triple", "triple_thick", ""};

/** Possible values for a rectangle corner type*/
template<> const char EnumEncapsulator<ECornerType>::names[][ENUM_STRING_LEN] =
    {"invalid", "none", "round", "bevel", "note", ""};

/**Make the style fully specified using default line style values.
 * If any attribute is empty, we set it to the default value
 * (Solid line, black color, width of 1, zero radius and no corner.)
 * All attributes are eventually set.
 * No change made to an already fully specified style.*/
void LineAttr::MakeComplete() noexcept
{
    if (!type) { type = ELineType::SOLID;}
    if (!color) { color = ColorType(0,0,0); }
    if (!width) {width = 1;}
    if (!corner) {corner = ECornerType::NONE;}
    if (!radius) {radius = 0;}
}


constexpr unsigned LineAttr::MAX_PATTERN_LEN;

unsigned LineAttr::DashPattern(std::span<double> ret) const noexcept
{
    //last number is sum of the ones before
    static const double dash_dotted[]={2, 2, 0};
    static const double dash_dashed[]={6, 6, 0};
    static const double dash_long_dashed[]={12, 12, 0};
    static const double dash_dash_dot[]={6, 4, 2, 4, 0};
    static const double dash_solid[] ={0};
    _ASSERT(type && type!=ELineType::INVALID);
    const double *pattern;
    switch (*type) {
    case ELineType::DOTTED: pattern = dash_dotted; break;
    case ELineType::DASHED: pattern = dash_dashed; break;
    case ELineType::LONG_DASHED: pattern = dash_long_dashed; break;
    case ELineType::DASH_DOT: pattern = dash_dash_dot; break;
    default: pattern = dash_solid; break;
    }
    double lw = LineWidth();
    unsigned num;
    for (num = 0; *pattern; pattern++, num++)
        ret[num] = *pattern*lw;
    return num;
}

LineAttr &LineAttr::operator +=(const LineAttr&a) noexcept
{
    if (a.type) type = a.type;
    if (a.color) {
        if (color)
            *color += *a.color;
        else
            color = a.color;
    }
    if (a.width) width = a.width;
    if (a.corner) corner = a.corner;
    if (a.radius) radius = a.radius;
    return *this;
};

/** Take an attribute and apply it to us.
 *
 * We consider attributes ending with 'color', 'width', 'type', 'corner' and 'radius';
 * or any style at the current context in `chart`. We also accept the clearing of
 * an attribute if `t` is EStyleType::STYLE, that is for style definitions only.
 * At a problem, we generate an error into chart->Error.
 * @param [in] a The attribute to apply.
 * @param chart The chart we build.
 * @param [in] t The situation we set the attribute.
 * @returns True, if the attribute was recognized as ours (may have been a bad value though).*/
bool LineAttr::AddAttribute(const Attribute &a, Chart *chart, EStyleType t)
{
    if (a.type == EAttrType::STYLE)
        return false;
    if (a.EndsWith("color")) {
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t))
                color.reset();
            return true;
        }
        if (!chart->CheckColor(a)) return true;
        if (color)
            *color += chart->GetCurrentContext()->colors.GetColor(a.value);
        else
            color = chart->GetCurrentContext()->colors.GetColor(a.value);
        return true;
    }
    if (a.EndsWith("type")) {
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t))
                type.reset();
            return true;
        }
        if (ELineType t;  a.type == EAttrType::STRING && Convert(a.value, t)) {
            type = t;
            return true;
        } else
            a.InvalidValueError(CandidatesFor(t), chart->Error);
        return true;
    }
    if (a.EndsWith("width")) {
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t))
                width.reset();
            return true;
        }
        if (a.type == EAttrType::NUMBER && a.number>=0 && a.number<=10) {
            width = a.number;
            return true;
        }
        a.InvalidValueError("0..10", chart->Error);
        return true;
    }
    if (a.EndsWith("corner")) {
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t))
                corner.reset();
            return true;
        }
        if (ECornerType c; a.type == EAttrType::STRING && Convert(a.value, c)) {
            corner = c;
            if (!radius || *radius<=0)
                radius = 10;
            return true;
        } else
            a.InvalidValueError(CandidatesFor(c), chart->Error);
        return true;
    }
    if (a.EndsWith("radius")) {
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t))
                radius.reset();
            return true;
        }
        if (a.CheckType(EAttrType::NUMBER, chart->Error)) {
            radius = a.number;
            if (a.number<0)
                corner = ECornerType::NONE;
            else if (!corner || *corner== ECornerType::NONE)
                corner = ECornerType::ROUND;
            return true;
        }
        return true;
    }
    return false;
}

/** Add the attribute names we take to `csh`.*/
void LineAttr::AttributeNames(Csh &csh, std::string_view prefix)
{
    static const char * const names_descriptions[] =
    {"", nullptr,
    "color", "The color of the line.",
    "type", "The style of the line, such as dotted, dashed or double",
    "width", "The width of the line",
    "radius", "How wide the line turns in corners (only for some corner types)",
    "corner", "The style of turning at corners.",
    ""};
    csh.AddToHints(names_descriptions, csh.HintPrefix(COLOR_ATTRNAME).append(prefix), EHintType::ATTR_NAME);
}

/** Callback for drawing a symbol before line type names in the hints popup list box.
 * @ingroup hintpopup_callbacks*/
bool CshHintGraphicCallbackForLineType(Canvas *canvas, CshHintGraphicParam p, CshHintStore &)
{
    if (!canvas) return false;
    LineAttr line(ELineType(int(p)), ColorType(0,0,0), 1, ECornerType::NONE, 0);
    canvas->Line(XY(HINT_GRAPHIC_SIZE_X*0.2, HINT_GRAPHIC_SIZE_Y*0.2),
              XY(HINT_GRAPHIC_SIZE_X*0.8, HINT_GRAPHIC_SIZE_Y*0.8), line);
    return true;
}

/** Callback for drawing a symbol before corner type names in the hints popup list box.
 * @ingroup hintpopup_callbacks*/
bool CshHintGraphicCallbackForCornerType(Canvas *canvas, CshHintGraphicParam p, CshHintStore &)
{
    if (!canvas) return false;
    canvas->Clip(XY(HINT_GRAPHIC_SIZE_X*0.2, HINT_GRAPHIC_SIZE_Y*0.2),
              XY(HINT_GRAPHIC_SIZE_X*0.8, HINT_GRAPHIC_SIZE_Y*0.8));
    LineAttr line(ELineType::SOLID, ColorType(0,0,0), 2, ECornerType(int(p)), ceil(HINT_GRAPHIC_SIZE_Y*0.3));
    canvas->Line(Block(-HINT_GRAPHIC_SIZE_X, HINT_GRAPHIC_SIZE_X*0.7,
              HINT_GRAPHIC_SIZE_Y*0.3, HINT_GRAPHIC_SIZE_Y*2), line);
    canvas->UnClip();
    return true;
}

/** Add a list of possible attribute value names to `csh` for attribute `attr`.*/
bool LineAttr::AttributeValues(std::string_view attr, Csh &csh)
{
    if (CaseInsensitiveEndsWith(attr, "color")) {
        csh.AddColorValuesToHints(false);
        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "type")) {
        csh.AddToHints(EnumEncapsulator<ELineType>::names, nullptr, csh.HintPrefix(COLOR_ATTRVALUE),
                       EHintType::ATTR_VALUE, CshHintGraphicCallbackForLineType);
        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "width")) {
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable()+"<number in pixels>",
            "Specify the with of the line in pixels. For double and triple lines, this is not the total width of the line, just the width of one component line.",
            EHintType::ATTR_VALUE, false));
        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "corner")) {
        csh.AddToHints(EnumEncapsulator<ECornerType>::names,
                       nullptr, //graphics hints tell it all
                       csh.HintPrefix(COLOR_ATTRVALUE),
                       EHintType::ATTR_VALUE, CshHintGraphicCallbackForCornerType);
        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "radius")) {
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable()+"<number in pixels>",
            "Specify how wide the line turns at corners, such as rectangle corners.",
            EHintType::ATTR_VALUE, false));
        return true;
    }
    return false;
}

/** Print the line style to a string.*/
string LineAttr::Print(int) const
{
    string ss = "line(";
    if (type) ss << " type:" << PrintEnum(*type);
    if (width) ss << " width:" << *width;
    if (color) ss << " color:" << color->Print();
    return ss + ")";
}

//This one does not assume anything about wither the resulting rectangle should be the
//outer edge or inner edge of the line - just uses the radius value and coordinates
//as they are.
//"this->radius" corresponds to the radius at the middle of the line
//"x1, x2, y1, y2" corresponds to the midline -> this is what is returned
//For ECornerType::NOTE it creates the outer line only
Contour LineAttr::CreateRectangle_Midline(double x1, double x2, double y1, double y2, double r) const
{
    if (r==-1 && radius)
        r = *radius;
    if (r<=0 || !corner)
        return Contour(x1, x2, y1, y2);
    Contour ret;
    r = std::min(r, MaxRadius(x1, x2, y1, y2));
    if (r<0.01)
        return Contour(x1, x2, y1, y2);
    switch (*corner) {
    default:
        return Contour(x1, x2, y1, y2);
    case ECornerType::ROUND:
        {
            Path edges;
            edges.reserve(8);

            edges.AppendEllipse(XY(x2-r, y1+r), r, r, 0, 270, 360, true, false);
            edges.AppendEllipse(XY(x2-r, y2-r), r, r, 0, 0, 90, true, true);
            edges.AppendEllipse(XY(x1+r, y2-r), r, r, 0, 90, 180, true, true);
            edges.AppendEllipse(XY(x1+r, y1+r), r, r, 0, 180, 270, true, true);
            ret.assign_dont_check(edges, ECloseType::CLOSE_ALL_CONNECTED);
            break;
        }
    case ECornerType::BEVEL:
        {
        const XY points[] = {
            XY(x1+r, y1),
            XY(x2-r, y1),
            XY(x2, y1+r),
            XY(x2, y2-r),
            XY(x2-r, y2),
            XY(x1+r, y2),
            XY(x1, y2-r),
            XY(x1, y1+r)};
        ret.assign_dont_check(points);
        break;
        }
    case ECornerType::NOTE:
        {
        const XY points[] = {
            XY(x1, y1),
            XY(x2-r, y1),
            XY(x2, y1+r),
            XY(x2, y2),
            XY(x1, y2)};
        ret.assign_dont_check(points);
        break;
        }
    }
    return ret;
}

/** Returns the fill area of a note-styled rectangle for double or triple line styles.
 *
 * The function assumes double or triple line style and ECornerType::NOTE corner style. It
 * considers line style, see an example below for
 * single, double and triple line styles.
 * @verbatim

   +------\       +----------\        +--------------\
   |       \      | +----+ |\ \       | +-----------\ \
   |       |      | |    | +-\ \      | | +-------+ |\ \
   +-------+      | |    +----+ \     | | |       | +-\ \
                  | |         | |     | | |       +--+ \ \
                  | +---------+ |     | | |          | | |
                  +-------------+     | | +----------+ | |
                                      | +--------------+ |
                                      +------------------+
   @endverbatim
 * It returns two SimpleContours, one L-shaped rectangle and the midline
 * of the inner line of the double line; plus a small triangle, also at the middle of its line.
 * ret[0] will be the body and ret[1] will be the triangle.
 * At call time `this->radius` corresponds to the radius at the middle of the (double or triple) line.
 * `x1, x2, y1, y2` corresponds to the midline of the rectangle.*/
Contour LineAttr::CreateRectangle_ForFill_Note(double x1, double x2, double y1, double y2) const
{
    _ASSERT(IsComplete());
    _ASSERT(corner == ECornerType::NOTE);
    _ASSERT(IsDoubleOrTriple());
    Block bb(x1, x2, y1, y2);
    const double s = Spacing();
    const double r1 = *radius;
    const double r2 = std::max(0., r1 - s*::RadiusIncMultiplier(*corner));
    const double r3 = r2 + 2**width;
    Contour ret;
    if (IsDouble()) {
        bb.Expand(-s);
        const XY c2[] = {XY(bb.x.till - r2, bb.y.from + r2),
                         XY(bb.x.till - r2, bb.y.from),
                         XY(bb.x.till     , bb.y.from + r2)};
        const XY c1[] = {bb.UpperLeft(),
                         XY(bb.x.till - r3, bb.y.from),
                         XY(bb.x.till - r3, bb.y.from + r3),
                         XY(bb.x.till     , bb.y.from + r3),
                         bb.LowerRight(),
                         bb.LowerLeft()};
        ret.assign_dont_check(c1);
        ret.append_dont_check(c2);
    } else {
        //IsTriple()
        const XY c2[] = {XY(bb.x.till - r1, bb.y.from + r1),
                         XY(bb.x.till - r1, bb.y.from),
                         XY(bb.x.till     , bb.y.from + r1)};
        bb.Expand(-s);
        const XY c1[] = {bb.UpperLeft(),
                         XY(bb.x.till - r1, bb.y.from),
                         XY(bb.x.till - r1, bb.y.from + r1),
                         XY(bb.x.till     , bb.y.from + r1),
                         bb.LowerRight(),
                         bb.LowerLeft()};
        ret.assign_dont_check(c1);
        ret.append_dont_check(c2);
    }
    return ret;
}

/** Returns the inner area of a note-styled rectangle.
 *
 * The function assumes ECornerType::NOTE corner style. It
 * considers line style, see an example below for
 * single, double and triple line styles.
 * @verbatim

   +------\       +----------\        +--------------\
   |       \      | +----+ |\ \       | +-----------\ \
   |       |      | |    | +-\ \      | | +-------+ |\ \
   +-------+      | |    +----+ \     | | |       | +-\ \
                  | |         | |     | | |       +--+ \ \
                  | +---------+ |     | | |          | | |
                  +-------------+     | | +----------+ | |
                                      | +--------------+ |
                                      +------------------+
   @endverbatim
 * It returns one SimpleContour, the copped one for single lines and one L-shaped rectangle
 * for double and triple line styles. The returned shape represents the inner edge of the line
 * surrounding it, thus inside it, nothing will be painted when we draw the line of the rectangle.
 * At call time `this->radius` corresponds to the radius at the middle of the (double or triple) line.
 * `x1, x2, y1, y2` corresponds to the midline of the rectangle.*/
Contour LineAttr::CreateRectangle_InnerEdge_Note(double x1, double x2, double y1, double y2) const
{
    _ASSERT(IsComplete());
    _ASSERT(corner == ECornerType::NOTE);
    Block bb(x1, x2, y1, y2);
    LineAttr line2(*this);
    const double lw2 = line2.LineWidth()/2;
    bb.Expand(-lw2);
    line2.Expand(-lw2);
    const double r = *line2.radius + 2**line2.width;
    const XY c1[] = {bb.UpperLeft(),
                     XY(bb.x.till - r, bb.y.from),
                     XY(bb.x.till - r, bb.y.from + r),
                     XY(bb.x.till    , bb.y.from + r),
                     bb.LowerRight(),
                     bb.LowerLeft()};
    Contour ret;
    ret.assign_dont_check(c1);
    return ret;
}


Contour LineAttr::CreateRectangle_ForFill(double x1, double x2, double y1, double y2) const
{
    if (!IsDoubleOrTriple())
        return CreateRectangle_Midline(x1, x2, y1, y2);
    if (corner != ECornerType::NOTE) { //We have double or triple line here
        const double s = Spacing();
        LineAttr tmp(*this);
        tmp.Expand(-s);
        if (corner == ECornerType::BEVEL && *tmp.radius > MaxRadius(std::max(0., fabs(x1-x2)-2*s), std::max(0., fabs(y1-y2)-2*s))) //special case when bevel becomes too small.
            return CreateRectangle_Midline(x1, x2, y1, y2).Expand(-s);
        return tmp.CreateRectangle_Midline(x1+s, x2-s, y1+s, y2-s);
    }
    return CreateRectangle_ForFill_Note(x1, x2, y1, y2);
}

Contour LineAttr::CreateRectangle_InnerEdge(double x1, double x2, double y1, double y2) const
{
    if (corner == ECornerType::NOTE)
        return CreateRectangle_InnerEdge_Note(x1, x2, y1, y2);
    const double lw2 = LineWidth()/2;
    return CreateRectangle_Midline(x1+lw2, x2-lw2, y1+lw2, y2-lw2);
}




/** Calculates how wide a rectangle should be when drawing one around a text label.
 *
 * See the situation below. @verbatim
      /---------------\
     /AAAAAAAAAAAAAAAAA\
    /                   \
   /                     \
   |                     |
   @endverbatim
 * Here we see the top of a beveled rectangle with a label 'AAAAAAAAA' in it.
 * We can see that the vertical sides of the rectangle are spaced further apart than
 * the length of the label - this is because of the beveled corners.
 *
 * In this function the label is already laid out on the surface - its extent is captured in `textCover`.
 * We want the very top of the rectangle (the outer edge of its upper horizontal line)
 * to be at the y coordinate `rect_top`. The rectangle is supposed to have corner style and radius
 * as specified in `this` along with the line width. (Radius corresponds to the midline.)
 * And checks how much margin the text needs form the _outer_edge_ of the rectangle
 * This is at least lineWidth() (if radius==0)
 * This one assumes that the
 * @param [in] textCover The extents of the label
 * @param [in] rect_top The y coordinate of the outer edge of the upper horizontal line of the rectangle.
 * @returns The left and right margin to be used from the leftmost and rightmost point of `textCover`, resp.
 *          till the *outer* edge of the vertical sides of the rectangle.
 *          This margin is at least as much as the line width then. We return (0,0) if `textCover` is empty.
 */
DoublePair LineAttr::CalculateTextMargin(Contour textCover, double rect_top) const
{
    _ASSERT(IsComplete());
    DoublePair ret(0,0);
    if (textCover.IsEmpty()) return ret;
    const double lw = LineWidth();
    if (*radius <= LineWidth() || corner==ECornerType::NONE) return DoublePair(lw, lw);
    //create a path at the inner edge of the rectangle
    const XY lr = textCover.GetBoundingBox().LowerRight();
    const Block inner(lw/2, *radius*3, rect_top+lw/2, rect_top+lw/2 +lr.y+*radius*2);
    const Contour inner_area = CreateRectangle_InnerEdge(inner); //the radius we have in the style luckily corresponds to the line midpoint
    const Range left_right = textCover.GetBoundingBox().x;
    textCover.Rotate(90);

    double off, tp;
    //left margin
    Contour a = Contour(0, inner.x.MidPoint(), inner.y.from-1, inner.y.till+1) - inner_area;
    a.Rotate(90);
    off = a.OffsetBelow(textCover, tp, CONTOUR_INFINITY);
    ret.left = left_right.from - off;
    //right margin
    a = Contour(inner.x.MidPoint(), inner.x.till+lw, inner.y.from-1, inner.y.till+1) - inner_area;
    a.Rotate(90);
    off = textCover.OffsetBelow(a, tp, CONTOUR_INFINITY);
    ret.right = inner.x.till-off-left_right.till + lw;
    return ret;
}


    //EGradientType::INVALID = 0,  ///<The invalid value
    //EGradientType::NONE, ///<No gradient in fill - single color only
    //EGradientType::OUTWARD,  ///<Radial gradient outwards from center
    //EGradientType::INWARD,   ///<Radial gradient inwards toward center
    //EGradientType::DOWN, ///<Linear gradient downwards
    //EGradientType::UP,   ///<Linear gradient upwards
    //EGradientType::LEFT, ///<Linear gradient leftwards
    //EGradientType::RIGHT,///<Linear gradient rightwards
    //EGradientType::BUTTON///<Linear gradient with multiple changes creating a button effect

/** Gives the gradient angle for a given gradient style. */
const double FillAttr::gradient_angl[] = {0, 0, 0, -1, 270, 90, 0, 180, 0};

/** Gives the internal gradient type for a given gradient style. */
const EIntGradType FillAttr::gradient_tr[] = {EIntGradType::INVALID, EIntGradType::NONE,
                                              EIntGradType::RADIAL, EIntGradType::RADIAL,
                                              EIntGradType::LINEAR, EIntGradType::LINEAR,
                                              EIntGradType::LINEAR, EIntGradType::LINEAR,
                                              EIntGradType::BUTTON};

/**Make the style fully specified using default fill style values.
 * The default value is white color, no gradients (used for unset attributes).
 * We leave `color2` as is.
 * (Fill is considered complete even if `color2` is not set, because then
 * we derive it from the primary color.)
 * All attributes are eventually set, except perhaps `color2`.
 * No change made to an already fully specified style.*/
void FillAttr::MakeComplete() noexcept
{
    if (!color) color = ColorType(255,255,255);
    //color2 is not needed for completeness
    if (!gradient_angle) gradient_angle = 0;
    if (!gradient) gradient = EIntGradType::NONE;
    if (!frac) frac = 0.5;
}


FillAttr &FillAttr::operator +=(const FillAttr&a) noexcept
{
    if (a.color) {
        if (color)
            *color += *a.color;
        else
            color = a.color;
    }
    if (a.color2) {
        if (color2)
            *color2 += *a.color2;
        else
            color2 = a.color2;
    }
    if (a.gradient_angle) gradient_angle = a.gradient_angle;
    if (a.gradient) gradient = a.gradient;
    if (a.frac) frac = a.frac;
    return *this;
};

/** Possible values for fill gradients.*/
template<> const char EnumEncapsulator<EGradientType>::names[][ENUM_STRING_LEN] =
    {"invalid", "none", "out", "in", "down", "up", "left", "right", "button", ""};

/** Take an attribute and apply it to us.
 *
 * We consider attributes ending with 'color', 'color2' and 'gradient';
 * or any style at the current context in `chart`. We also accept the clearing of
 * an attribute if `t` is EStyleType::STYLE, that is for style definitions only.
 * At a problem, we generate an error into chart->Error.
 * @param [in] a The attribute to apply.
 * @param chart The chart we build.
 * @param [in] t The situation we set the attribute.
 * @returns True, if the attribute was recognized as ours (may have been a bad value though).*/
bool FillAttr::AddAttribute(const Attribute &a, Chart *chart, EStyleType t)
{
    if (a.type == EAttrType::STYLE)
        return false;
    if (a.EndsWith("color") || a.EndsWith("color2")) {
        OptAttr<ColorType> &c = (a.name[a.name.length()-1]=='2') ? color2 : color;
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t))
                c.reset();
            return true;
        }
        if (!chart->CheckColor(a)) return true;
        if (c)
            *c += chart->GetCurrentContext()->colors.GetColor(a.value);
        else
            c = chart->GetCurrentContext()->colors.GetColor(a.value);
        return true;
    }
    if (a.EndsWith("gradient")) {
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t))
                gradient_angle.reset();
            return true;
        }
        EGradientType t = EGradientType::NONE; //we initialize to avoid runtime checks below
        if (a.type == EAttrType::STRING && Convert(a.value, t)) {
            gradient_angle = gradient_angl[(unsigned)t];
            gradient = gradient_tr[(unsigned)t];
        } else if (a.type==EAttrType::NUMBER) {
            if (a.number<0 || a.number>=360)
                chart->Error.Error(a, true , "Gradient angle must be between [0..360). Ignoring attribute.");
            else {
                gradient_angle = a.number;
                gradient = EIntGradType::LINEAR;
            }
        } else
            a.InvalidValueError(CandidatesFor(t), chart->Error);
        return true;
    }
    if (a.EndsWith("fraction")) { //yet undocumented
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t))
                frac.reset();
        } else if (a.type==EAttrType::NUMBER && a.number>0 && a.number<1)
            frac = a.number;
        else
            chart->Error.Error(a, true, "Fraction must be between (0..360). Ignoring attribute.");
        return true;
    }
    return false;
}

/** Callback for drawing a symbol before gradient names in the hints popup list box.
 * @ingroup hintpopup_callbacks*/
bool CshHintGraphicCallbackForGradient(Canvas *canvas, CshHintGraphicParam p, CshHintStore &)
{
    if (!canvas) return false;
    const int size = HINT_GRAPHIC_SIZE_Y-2;
    const int off_x = (HINT_GRAPHIC_SIZE_X - size)/2;
    const int off_y = 1;
    ColorType black(0,0,0);
    FillAttr fill(black, ColorType(255,255,255), EGradientType(int(p)));
    LineAttr line(ELineType::SOLID, black, 1, ECornerType::NONE, 0);
    Block rect(XY(off_x, off_y), XY(off_x+size, off_y+size));
    rect.Round().Shift(XY(.5,.5));
    canvas->Fill(rect, fill);
    canvas->Line(rect, line);
    return true;
}


/** Add the attribute names we take to `csh`.*/
void FillAttr::AttributeNames(Csh &csh, std::string_view prefix)
{
    static const char * const names_descriptions[]=
    {"", nullptr,
    "color", "The color of the fill.",
    "color2", "The second color of the fill (only for two-color gradients).",
    "gradient", "The gradient type of the fill.",
    ""};
    csh.AddToHints(names_descriptions, csh.HintPrefix(COLOR_ATTRNAME).append(prefix), EHintType::ATTR_NAME);
}

/** Add a list of possible attribute value names to `csh` for attribute `attr`.*/
bool FillAttr::AttributeValues(std::string_view attr, Csh &csh)
{
    if (CaseInsensitiveEndsWith(attr, "color") || CaseInsensitiveEndsWith(attr, "color2")) {
        csh.AddColorValuesToHints(false);
        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "gradient")) {
        csh.AddToHints(EnumEncapsulator<EGradientType>::names, nullptr, csh.HintPrefix(COLOR_ATTRVALUE),
                       EHintType::ATTR_VALUE, CshHintGraphicCallbackForGradient);
        return true;
    }
    return false;
}

/** Flips the direction of the gradient.
 @param updown If true, we flip up/down, if false we flip left/right.*/
void FillAttr::FlipGradient(bool updown) noexcept
{
    if (gradient_angle) {
        if (gradient_angle == -4)
            *gradient_angle = 90; //substitute button with "UP"
        if (*gradient_angle > 0) {
            //for linear fill, flip angle
            if (updown)
                *gradient_angle = 360 - *gradient_angle;
            else //left-right
                *gradient_angle = fmod(360 + 180 - *gradient_angle, 360);
        }
    }
}

/** Rotate the angle of the gradient by 'deg' degrees.*/
void FillAttr::RotateGradient(double deg) noexcept
{
    if (gradient_angle) {
        if (*gradient_angle== -4)
            *gradient_angle= 90; //substitute button with "UP"
        if (*gradient_angle> 0)
            //for linear fill, rotate angle
            *gradient_angle = fmod(*gradient_angle+deg+360, 360);
    }
}

/** Print the line style to a string.*/
string FillAttr::Print(int) const
{
    string ss = "fill(";
    if (color) ss << " color:" << color->Print();
    if (color2) ss << " color2:" << color2->Print();
    if (gradient_angle) ss << " gradient:" << *gradient_angle;
    return ss + ")";
}

/** If this fill is applied to a rectangle, what will be the typical fill
 * at a horizontal line of a rectangle. Call only if fully specified.
 * @param [in] pos If 0 we show what is the fill at the top of the rectangle
 *             If 1, it will be the bottom of the rectangle. [0..1] is OK.*/
FillAttr FillAttr::ApproxColorHorizontal(double pos) const noexcept {
    switch (gradient.value_or(EIntGradType::NONE)) {
    default:
    case EIntGradType::INVALID:
    case EIntGradType::NONE:
    case EIntGradType::BUTTON:
        return FillAttr{*color, EGradientType::NONE};
    case EIntGradType::RADIAL:
        return FillAttr{*gradient_angle<0 ? *color2: *color, EGradientType::NONE};
    case EIntGradType::DEGENERATE:
    case EIntGradType::LINEAR:
        if (*gradient_angle==0 || *gradient_angle==180)
            return *this;
        else if (*gradient_angle==90)
            return FillAttr{color->MixWith(*color2, pos), EGradientType::NONE};
        else if (gradient_angle==270)
            return FillAttr{color2->MixWith(*color, pos), EGradientType::NONE};
        else if (gradient_angle<180)
            return FillAttr{*color2, EGradientType::NONE};
        else
            return FillAttr{*color, EGradientType::NONE};
    }
}


/**Make the style fully specified using default shadow style values.
 * The default value is black color, no offset, no blur.
 * All attributes are eventually set.
 * No change made to an already fully specified style.*/
void ShadowAttr::MakeComplete() noexcept
{
    if (!color) color = ColorType(0,0,0);
    if (!offset) offset = 0;
    if (!blur) blur = 0;
}

ShadowAttr &ShadowAttr::operator +=(const ShadowAttr&a) noexcept
{
    if (a.color) {
        if (color)
            *color += *a.color;
        else
            color = a.color;
    }
    if (a.offset) offset = a.offset;
	if (a.blur) blur = a.blur;
    return *this;
};

/** Take an attribute and apply it to us.
 *
 * We consider attributes ending with 'color', 'offset' and 'blur';
 * or any style at the current context in `chart`. We also accept the clearing of
 * an attribute if `t` is EStyleType::STYLE, that is for style definitions only.
 * At a problem, we generate an error into chart->Error.
 * @param [in] a The attribute to apply.
 * @param chart The chart we build.
 * @param [in] t The situation we set the attribute.
 * @returns True, if the attribute was recognized as ours (may have been a bad value though).*/
bool ShadowAttr::AddAttribute(const Attribute &a, Chart *chart, EStyleType t)
{
    if (a.type == EAttrType::STYLE)
        return false;
    if (a.EndsWith("color")) {
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t))
                color.reset();
            return true;
        }
        if (!chart->CheckColor(a)) return true;
        if (color)
            *color += chart->GetCurrentContext()->colors.GetColor(a.value);
        else
            color = chart->GetCurrentContext()->colors.GetColor(a.value);
        return true;
    }
    if (a.EndsWith("offset")) {
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t))
                offset.reset();
            return true;
        }
        if (a.type == EAttrType::NUMBER && a.number>=0 && a.number<=100) {
            offset = a.number;
            return true;
        }
        a.InvalidValueError("0..100", chart->Error);
        return true;
    }
    if (a.EndsWith("blur")) {
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t))
                blur.reset();
            return true;
        }
        if (a.type == EAttrType::NUMBER && a.number>=0 && a.number<=100) {
            blur = a.number;
            return true;
        }
        a.InvalidValueError("0..100", chart->Error);
        return true;
    }
    return false;
}

/** Add the attribute names we take to `csh`.*/
void ShadowAttr::AttributeNames(Csh &csh)
{
    static const char * const names_descriptions[] =
    {"", nullptr,
    "shadow.color", "The color of the shadow (at its darkest).",
    "shadow.offset", "The width of the shadow in pixels, that is, how 'high' the element appears above the background.",
    "shadow.blur", "How much of the shadow is blurred in pixels. Zero means no blur.",
    ""};
    csh.AddToHints(names_descriptions, csh.HintPrefix(COLOR_ATTRNAME), EHintType::ATTR_NAME);
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME)+"shadow.*",
        "Options for the shadow of the element.",
        EHintType::ATTR_NAME));
}

/** Add a list of possible attribute value names to `csh` for attribute `attr`.*/
bool ShadowAttr::AttributeValues(std::string_view attr, Csh &csh)
{
    if (CaseInsensitiveEndsWith(attr, "color")) {
        csh.AddColorValuesToHints(false);
        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "offset")) {
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable()+"<number in pixels>",
            "The width of the shadow in pixels, that is, how 'high' the element appears above the background.",
            EHintType::ATTR_VALUE, false));
        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "blur")) {
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable()+"<number in pixels>",
            "Specify how much of the shadow is blurred in pixels. Zero means no blur.",
            EHintType::ATTR_VALUE, false));
        return true;
    }
    return false;
}


/** Print the line style to a string.*/
string ShadowAttr::Print(int) const
{
    string ss = "shadow(";
    if (color) ss << " color:" << color->Print();
    if (offset) ss << " offset:" << *offset;
    if (blur) ss << " blur:" << *blur;
    return ss + ")";
}


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

/** Callback for drawing a symbol before arrowhead type names for line arrows in the hints popup list box.
 * @ingroup hintpopup_callbacks*/
bool CshHintGraphicCallbackForArrows(Canvas *canvas, EArrowType type, EArrowSize size, bool left);


/** Draws a block arrow and a line it points to
 * @param canvas The canvas to draw onto. We assume in it is HINT_GRAPHIC_SIZE_X, HINT_GRAPHIC_SIZE_Y big.
 * @param [in] type The type of the block arrow
 * @param [in] tip The coordinates of the block arrowhead
 * @param [in] width the width of the body
 * @param [in] dir The direction of the arrow:0: ->, 1: <-, 2: v, 3: ^, defaults to 0
 * @param [in] color the color of the arrow. Fill will be 0.7 lighter than this. Defaults to light green
 * @param [in] size The size compared to normal (this is the size of the fins) defaults to very small
 * @param [in] eLine The line the arrow points to. Defaults to none.
 * @returns true.*/
bool CshHintGraphicCallbackForBigArrowsDetailed(Canvas *canvas, EArrowType type, const XY &tip, double width,
                                                int dir, ColorType color, const XY &size, const LineAttr &eLine)
{
    const Block b(-100, tip.x, tip.y-width/2, tip.y+width/2);  //as if dir ==0
    SingleArrowHead ah(type, true, size);
    LineAttr line(ELineType::SOLID, color);
    FillAttr fill(line.color->Lighter(0.7), EGradientType::UP);
    ShadowAttr shadow;

    //calculate arrow contour
    Contour area = ah.BlockContour(b.x.till, b.y.from, b.y.till, line, false);
    auto who = ah.BlockWidthHeight(b.y.Spans(), line, false);
    area += Block(b.x.from, b.x.till-who.body_connect_before, b.y.from, b.y.till);

    //draw
    canvas->Clip(XY(HINT_GRAPHIC_SIZE_X*0.1, 1), XY(HINT_GRAPHIC_SIZE_X-1, HINT_GRAPHIC_SIZE_Y-1));
    switch (dir) {
    default: _ASSERT(0); FALLTHROUGH;
    case 0:
    case 1:
        canvas->Line(XY(tip.x, 1), XY(tip.x, HINT_GRAPHIC_SIZE_Y-1), eLine);
        if (dir==1)
            area.RotateAround(tip, 180);
        break;
    case 2:
    case 3:
        canvas->Line(XY(1, tip.y ), XY(HINT_GRAPHIC_SIZE_X-1, tip.y), eLine);
        if (dir==2)
            area.RotateAround(tip, 90);
        else
            area.RotateAround(tip, 270);
        break;
    }

    area.Expand(line.LineWidth()/2); //get midline
    canvas->Shadow(area, shadow);
    canvas->Fill(area.CreateExpand(line.Spacing()), fill);
    canvas->Line(area, line);
    canvas->UnClip();
    return true;
}



/** Callback for drawing a symbol before arrowhead type names for block arrows in the hints popup list box.
 * @ingroup hintpopup_callbacks*/
bool CshHintGraphicCallbackForBigArrows(Canvas *canvas, CshHintGraphicParam p, CshHintStore&)
{
    if (!canvas) return false;
    if (!GoodForBlockArrows((EArrowType)(int)p)) {
        if (GoodForArrows((EArrowType)(int)p))
            return CshHintGraphicCallbackForArrows(canvas, (EArrowType)(int)p, EArrowSize::SMALL, false);
        else return false;
    }
    return CshHintGraphicCallbackForBigArrowsDetailed(canvas, (EArrowType)(int)p, XY(HINT_GRAPHIC_SIZE_X*0.7, HINT_GRAPHIC_SIZE_Y*0.5));
}

bool CshHintGraphicCallbackForArrows(Canvas *canvas, EArrowType type, EArrowSize size, bool left)
{
    if (!canvas) return false;
    LineAttr eLine(ELineType::SOLID, ColorType(0, 0, 0), 1, ECornerType::NONE, 0);
    LineAttr aLine(ELineType::SOLID, ColorType(0, 0, 0), 1, ECornerType::NONE, 0);
    ArrowHead ah(type, false, size);
    auto who = ah.WidthHeight(aLine, aLine);
    const double xx = left ?
        who.after_x_extent+1+eLine.LineWidth()/2 :
        HINT_GRAPHIC_SIZE_X-1-who.after_x_extent-eLine.LineWidth()/2;
    XY xy(xx, HINT_GRAPHIC_SIZE_Y/2);
    Range cover = ah.TargetCover(xy, false, aLine, aLine).GetBoundingBox().y;
    canvas->Clip(XY(1,1), XY(HINT_GRAPHIC_SIZE_X-1, HINT_GRAPHIC_SIZE_Y-1));
    if (cover.IsInvalid()) {
        canvas->Line(XY(xy.x, 1), XY(xy.x, HINT_GRAPHIC_SIZE_Y-1), eLine);
    } else {
        if (cover.from>1)
            canvas->Line(XY(xy.x, 1), XY(xy.x, cover.from), eLine);
        if (cover.till<HINT_GRAPHIC_SIZE_Y-1)
            canvas->Line(XY(xy.x, cover.till), XY(xy.x, HINT_GRAPHIC_SIZE_Y-1), eLine);
    }
    Contour clip = ah.ClipForLine(xy, false, true, aLine, aLine);
    canvas->ClipInverse(clip);
    if (left)
        canvas->Line(XY(HINT_GRAPHIC_SIZE_X*0.9, xy.y), xy, aLine);
    else
        canvas->Line(XY(HINT_GRAPHIC_SIZE_X*0.1, xy.y), xy, aLine);
    canvas->UnClip();
    ah.Draw(*canvas, xy, false, aLine, aLine);
    canvas->UnClip();
    return true;
}

/** Callback for drawing a symbol before arrowhead type names (in general) in the hints popup list box.
 * @ingroup hintpopup_callbacks*/
bool CshHintGraphicCallbackForArrowTypes(Canvas *canvas, CshHintGraphicParam p, CshHintStore&csh)
{
    if (!GoodForArrows((EArrowType)(int)p)) {
        if (GoodForBlockArrows((EArrowType)(int)p))
            return CshHintGraphicCallbackForBigArrows(canvas, p, csh);
        else return false;
    }
    return CshHintGraphicCallbackForArrows(canvas, (EArrowType)(int)p, EArrowSize::SMALL, false);
}

/** Callback for drawing a symbol before arrowhead size names in the hints popup list box.
 * @ingroup hintpopup_callbacks*/
bool CshHintGraphicCallbackForArrowSizes(Canvas *canvas, CshHintGraphicParam p, CshHintStore&)
{
    return CshHintGraphicCallbackForArrows(canvas, EArrowType::SOLID, (EArrowSize)(int)p, true);
}

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

template<> const char EnumEncapsulator<EArrowType>::names[][ENUM_STRING_LEN] =
    {"invalid", "none",
     "solid", "empty", "line", "half",
     "diamond", "empty_diamond", "dot", "empty_dot",
     "sharp", "empty_sharp", "vee", "empty_vee",
     "nsdiamond", "empty_nsdiamond", "nsdot", "empty_nsdot",
     "jumpover",
     "double", "double_empty", "double_line", "double_half",
     "triple", "triple_empty", "triple_line", "triple_half",
     "empty_inv", "stripes", "triangle_stripes", ""};
template<> const char * const EnumEncapsulator<EArrowType>::descriptions[] =
{nullptr,
     "No arrowhead, just a line (or rectangle for block arrows).",
     "Arrows: a filled equilateral triangle.\nBlock arrows: a triangle wider than the body.",
     "Arrows: A line-only equilateral triangle.\nBlock arrows: A triangle of same with as the body.",
     "Arrows: Two-lines.\nBlock arrows: a triangle wider than the body.",
     "Arrows: Just one line.\nBlock arrows: a lop-sided triangle.",
     "Arrows: A filled diamond shape at the center of the target.\nBlock arrows:A diamond shape wider than the body.",
     "Arrows: A line-only diamond at the center of the target.\nBlock arrows:A triangle of same with as the body, extending beyond the entity line.",
     "Arrows: A filled circle at the center of the target.\nBlock arrows:A circle wider than the body.",
     "Arrows: A line-only circle at the center of the target.\nBlock arrows:A round ending.",
     "Arrows: A filled acute triangle.\nBlock arrows:An acute triangle.",
     "Arrows: A line-only acute triangle.\nBlock arrows: A triangle of same with as the body.",
     "A shape like a '<' sign (from graphviz).",
     "A shape like a '<' sign, but empty inside.",
     "A filled diamond shape only touching the target.",
     "A line-only diamond only touching the target.",
     "A filled circle only touching the target.",
     "A line-only circle only touching the target.",
     "A little half circle, jumping over the entity line indicating that we skip this entity. Best used for arrow.skipType.",
     "Two filled triangles.", "Two line-only triangles.", "Two times two lines.", "Two times just one line.",
     "Three filled triangles.", "Three line-only triangles.", "Three times two lines.", "Three times just one line.",
     "An inverse (cut-out) equilateral triangle.", "Two straight stripes.", "Two bent stripes.", ""};

template<> const char EnumEncapsulator<EArrowSize>::names[][ENUM_STRING_LEN] =
    {"invalid", "epsilon", "spot", "tiny", "small", "normal", "big", "huge", ""};
template<> const char * const EnumEncapsulator<EArrowSize>::descriptions[] = {
    nullptr, "13% of normal", "27% of normal", "40% of normal.", "66% of normal.", "What feels just right.", "133% of normal.", "173% of normal.", ""};


/** Set the specified OptAttr<EArrowType> to the value of the attribute.
 * Gives errors if arrow type named is not applicable for our 'type'.
 * Returns true if we actually have made a change.*/
bool OneArrowAttr::SetArrowType(const Attribute & a, Chart * chart, EStyleType t, OptAttr<EArrowType>& attr)
{
    if (a.type == EAttrType::CLEAR) {
        if (a.EnsureNotClear(chart->Error, t)) {
            attr.reset();
            dirty = true;
        }
        return true;
    }
    EArrowType at;
    if (a.type == EAttrType::STRING &&
        Convert(a.value, at)) {
        if (type == EArcArrowType::ARROW && !GoodForArrows(at)) {
            chart->Error.Error(a, true, "Value '"+a.value+"' is applicable only to block arrows.");
            return false;
        }
        if (type == EArcArrowType::BIGARROW && !GoodForBlockArrows(at)) {
            chart->Error.Error(a, true, "Value '"+a.value+"' is not applicable to block arrows.");
            return false;
        }
        attr = at;
        dirty = true;
        return true;
    }
    a.InvalidValueError(CandidatesFor(at), chart->Error);
    return false;
}

/** Set the specified string to the value of the gv style arrowhead attribute.
 * Gives errors if arrow type definition is ill-formed or is not applicable for our 'type'.
 * Returns true if we actually have made a change.*/
bool OneArrowAttr::SetArrowType(const Attribute &a, Chart *chart, EStyleType t, OptAttr<std::string> &gvattr)
{
    if (a.type == EAttrType::CLEAR) {
        if (a.EnsureNotClear(chart->Error, t))
            gvattr.reset();
        return true;
    }
    //When parsing procedures, colors are unknown, so we accept all text. It will be sanitized when the procedure is called.
    if (chart->SkipContent()) return true;
    std::string replacement;
    //we can use whatever scale and line below, as we do not actually generate an ArrowHead, just
    //sanitize the string.
    if (!ArrowHead::ParseGraphviz(a.value.c_str(), type, line, XY(0, 0), &chart->GetCurrentContext()->colors,
                                  a.linenum_value.start, chart, nullptr, &replacement)) {
        gvattr = std::move(replacement);
        return true;
    }
    return false;
}

void OneArrowAttr::ApplyArrowType(OptAttr<EArrowType>& my_type, const OptAttr<EArrowType>& other_type,
                                  OptAttr<std::string> &my_gvtype, const OptAttr<std::string> &other_gvtype,
                                  EArcArrowType other_t)
{
    if (other_type && GoodForThisArrowType(type, *other_type)) {
        my_type = other_type;
        my_gvtype.reset();
        dirty = true;
    }
    if (other_gvtype)
        if (type == EArcArrowType::ANY || other_t==type ||
            false==ArrowHead::ParseGraphviz(other_gvtype->c_str(), type)) {
            my_gvtype = other_gvtype;
            my_type.reset();
            dirty = true;
        }
}


void OneArrowAttr::CreateAnArrowHead(ArrowHead &ah, double scale,
                                     const OptAttr<EArrowType> &oldType,
                                     const OptAttr<std::string> &gvType) const
{
    _ASSERT(type==EArcArrowType::ARROW || type==EArcArrowType::BIGARROW);
    _ASSERT(oldType.has_value() != gvType.has_value());
    ah.clear();
    if (oldType) {
        ah.Append(*oldType, type==EArcArrowType::BIGARROW, GetScale()*scale, line);
        if (type==EArcArrowType::BIGARROW) {
            _ASSERT(GoodForBlockArrows(ah.front().type));
        } else {
            _ASSERT(GoodForArrows(ah.front().type));
        }
    } else if (gvType) {
        if (ArrowHead::ParseGraphviz(gvType->c_str(), type, line,
            GetScale()*scale, nullptr, FileLineCol(), nullptr, &ah, nullptr)) {
            _ASSERT(0);
        }
    }
}



void OneArrowAttr::Empty()
{
    line.Empty();
    size.reset();
    endType.reset();
    gvEndType.reset();
    xmul.reset();
    ymul.reset();
    dirty = true;
}


void OneArrowAttr::MakeComplete()
{
    if (!size) { size = EArrowSize::SMALL; dirty = true; }
    if (!xmul) {xmul = 1; dirty = true;}
    if (!ymul) {ymul = 1; dirty = true;}
    if (!endType && !gvEndType) { endType = EArrowType::SOLID; dirty = true; }
    if (line.IsComplete()) return;
    line.MakeComplete();
    dirty = true;
}

OneArrowAttr & OneArrowAttr::operator += (const OneArrowAttr &toadd)
{
    //here we always set dirty - but maybe we do not change the attribute
    //potential for improvement - but little, since we usually don't set the attribute
    //after CreateArrowHeads anyway.
    line += toadd.line;
    ApplyArrowType(endType, toadd.endType, gvEndType, toadd.gvEndType, toadd.type);
    if (toadd.size) size = toadd.size;
    if (toadd.xmul) xmul = toadd.xmul;
    if (toadd.ymul) ymul = toadd.ymul;
    dirty = true;
    return *this;
}

/** Take an Attribute object and add its value to us.
 * We recognize 'arrow', '*.type', '*.endtype',
 * '*.starttype', '*.size', 'arrowsize',
 * '*.xmul', '*.ymul', '*.color' and '*.line.width'.
 */
bool OneArrowAttr::AddAttribute(const Attribute &a, Chart *chart, EStyleType t)
{
    if (a.type == EAttrType::STYLE)
        return false;
    //do this first since ".type" ending is checked for below.
    if (a.EndsWith("color") || a.EndsWith("line.width") ||
        a.EndsWith("line.type") || a.EndsWith("line.corner")) {
        dirty = true;
        return line.AddAttribute(a, chart, t);
    }
    if ((accepts_arrow_attr && a.Is("arrow")) || a.EndsWith("type") ||
        a.EndsWith(StrCat(name,"type"))) {
        if (a.Is("arrow"))
            chart->Error.Warning(a, false, "Attribute 'arrow' is deprecated, but understood.",
                                "Use 'arrow.type'.");
        if (SetArrowType(a, chart, t, endType)) gvEndType.reset();
        return true;
    }
    if (a.EndsWith("gvtype") || a.EndsWith(StrCat("gv",name,"type"))) {
        if (SetArrowType(a, chart, t, gvEndType)) endType.reset();
        return true;
    }
    if ((accepts_arrow_attr && a.Is("arrowsize")) || a.EndsWith("size")) {
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t)) {
                size.reset();
                dirty = true;
            }
            return true;
        }
        if (EArrowSize s; a.type == EAttrType::STRING && Convert(a.value, s)) {
            size = s;
            dirty = true;
            if (a.Is("arrowsize"))
                chart->Error.Warning(a, false, "Attribute 'arrowsize' is deprecated, but understood.",
                                   "Use 'arrow.size'.");
            return true;
        } else
            a.InvalidValueError(CandidatesFor(s), chart->Error);
        return true;
    }
    if (a.EndsWith("xmul") || a.EndsWith("ymul")) {
        OptAttr<double> &mul = a.EndsWith("xmul") ? xmul : ymul;
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t)) {
                mul.reset();
                dirty = true;
            }
            return true;
        }
        if (a.type == EAttrType::NUMBER && a.number>=0.1 && a.number<=10) {
            mul= a.number;
            dirty = true;
            return true;
        }
        a.InvalidValueError("0.1 .. 10", chart->Error);
        return true;
    }
    return false;
}

void OneArrowAttr::AttributeNames(Csh &csh, std::string_view prefix)
{
    static const char * const names_descriptions[] ={
        "invalid", nullptr,
        "type", "Select the type of the arrowhead, such as sharp, solid, double, etc. A shorthand for setting both 'endtype' and 'midtype'.",
        "gvtype", "Specify the type of the arrowhead using graphviz syntax. A shorthand for setting both 'endtype' and 'midtype'.",
        "size", "The size of the arrowhead.",
        "color", "The color of the arrowheads",
        "type", "The arrowhead type to use at the destination of the arrow.",
        "line.width", "The line with of line-like arrowheads.",
        "line.color", "The color of the line of the arrowhead.",
        "line.type", "The line style of the line-like arrowheads, like dotted, dashed, etc.",
        "line.corner", "Determines, how to join arrowheads when on a bent line.",
        "xmul", "A size multiplier applied to the width arrowheads. To increase width, for example, specify a number greater than 1.",
        "ymul", "A size multiplier applied to the height arrowheads. To decrease height, for example, specify a number smaller than 1.",
        ""};
    csh.AddToHints(names_descriptions, csh.HintPrefix(COLOR_ATTRNAME).append(prefix), EHintType::ATTR_NAME);
}


bool OneArrowAttr::AttributeValues(std::string_view attr, Csh &csh, EArcArrowType t)
{
    if (CaseInsensitiveEqual(attr, "arrow") ||
        CaseInsensitiveEndsWith(attr, "type") ||
        CaseInsensitiveEndsWith(attr, "endtype")) {
        for (int i=1; EnumEncapsulator<EArrowType>::names[i][0]; i++)
            if (t==EArcArrowType::ANY ||
                (t==EArcArrowType::BIGARROW && GoodForBlockArrows(EArrowType(i))) ||
                (t==EArcArrowType::ARROW && GoodForArrows(EArrowType(i))))
                csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRVALUE) + EnumEncapsulator<EArrowType>::names[i],
                               EnumEncapsulator<EArrowType>::descriptions[i],
                               EHintType::ATTR_VALUE, true,
                               t==EArcArrowType::BIGARROW ? CshHintGraphicCallbackForBigArrows : CshHintGraphicCallbackForArrowTypes,
                               CshHintGraphicParam(i)));
        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "gvtype") ||
        CaseInsensitiveEndsWith(attr, "gvendtype")) {
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable() + "<graphviz syntax>",
            "A combination of basic graphviz arrowhead types "
            "(normal, diamond, dot, box, inv, vee, tee, line, crow, curve, icurve) "
            "prefixed with 'o' (for empty) and/or 'l'/'r' (for half side). "
            "You can use a few more non-graphviz types (sharp, sbox, sdiamond, sdot, the "
            "latter ones for 'symmetric' them being on the target line) and "
            "stripes, triangle_stripes for block arrows. "
            "In addition you can insert arrowhead size tokens and color names, "
            "e.g., 'tinyreddotsmallblacknormal'. "
            "(Use 'sizenormal' for 'normal' size which collides with 'normal' type.) "
            "Arrowheads may be separated by '|' pipe symbols, "
            "e.g., \"tinyreddot|small|black|normal\", but then use quotation marks around. "
            "In addition you can add attributes (always terminated by a pipe): "
            "'lwidth', 'ltype', 'color', 'xmul', 'ymul', 'mul' to adjust arrowheads after. "
            "('mul' numbers change arrowhead size by multiplying the current one.) "
            "E.g., \"xmul=0.5tee|xmul=2|ltype=dotted|line\". ",
            EHintType::ATTR_VALUE, false));
    }
    if (CaseInsensitiveEndsWith(attr, "xmul")) {
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable()+"<multiplier between [0.1 and 10]>",
            "Specify a number how much wider you want the arrowhead.",
            EHintType::ATTR_VALUE, false));
        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "ymul")) {
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable()+"<multiplier between [0.1 and 10]>",
            "Specify a number how much higher you want the arrowhead.",
            EHintType::ATTR_VALUE, false));
        return true;
    }
    if (CaseInsensitiveEqual(attr, "arrowsize") ||
        CaseInsensitiveEndsWith(attr, "size")) {
        csh.AddToHints(EnumEncapsulator<EArrowSize>::names, EnumEncapsulator<EArrowSize>::descriptions,
                       csh.HintPrefix(COLOR_ATTRVALUE),
                       EHintType::ATTR_VALUE, CshHintGraphicCallbackForArrowSizes);
        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "color") ||
        CaseInsensitiveEndsWith(attr, "line.type") ||
        CaseInsensitiveEndsWith(attr, "line.width") ||
        CaseInsensitiveEndsWith(attr, "line.corner")) {
                return LineAttr::AttributeValues(attr, csh);
    }
    return false;

}

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


void TwoArrowAttr::Empty()
{
    OneArrowAttr::Empty();
    startType.reset();
    gvStartType.reset();
}


void TwoArrowAttr::MakeComplete()
{
    OneArrowAttr::MakeComplete();
    if (!startType && !gvStartType) { startType = EArrowType::NONE; dirty = true; }
}

TwoArrowAttr & TwoArrowAttr::operator += (const TwoArrowAttr &toadd)
{
    OneArrowAttr::operator+=(toadd);
    ApplyArrowType(startType, toadd.startType, gvStartType, toadd.gvStartType, toadd.type);
    return *this;
}

/** Take an Attribute object and add its value to us.
 * We recognize 'arrow', '*.type', '*.endtype',
 * '*.starttype', '*.size', 'arrowsize',
 * '*.xmul', '*.ymul', '*.color' and '*.line.width'.
 */
bool TwoArrowAttr::AddAttribute(const Attribute &a, Chart *chart, EStyleType t)
{
    if (a.type == EAttrType::STYLE)
        return false;
    if (a.EndsWith("starttype")) {
        if (SetArrowType(a, chart, t, startType)) gvStartType.reset();
        return true;
    }
    if (a.EndsWith("gvstarttype")) {
        if (SetArrowType(a, chart, t, gvStartType)) startType.reset();
        return true;
    }
    return OneArrowAttr::AddAttribute(a, chart, t);
}

void TwoArrowAttr::AttributeNames(Csh &csh, std::string_view prefix)
{
    static const char * const names_descriptions[] ={
        "invalid", nullptr,
        "starttype", "The arrowhead type used where the arrow starts.",
        "endtype", "The arrowhead type to use at the destination of the arrow.",
        "gvstarttype", "The arrowhead type (expressed in graphviz style) used where the arrow starts.",
        "gvendtype", "The arrowhead type (expressed in graphviz style) to use at the destination of the arrow.",
        ""};
    csh.AddToHints(names_descriptions, csh.HintPrefix(COLOR_ATTRNAME).append(prefix), EHintType::ATTR_NAME);
    OneArrowAttr::AttributeNames(csh, prefix);
}


bool TwoArrowAttr::AttributeValues(std::string_view attr, Csh &csh, EArcArrowType t)
{
    if (CaseInsensitiveEndsWith(attr, "starttype"))
        return OneArrowAttr::AttributeValues("type", csh, t);
    if (CaseInsensitiveEndsWith(attr, "gvstarttype"))
        return OneArrowAttr::AttributeValues("gvtype", csh, t);

    return OneArrowAttr::AttributeValues(attr, csh, t);
}

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

void FullArrowAttr::Empty()
{
    TwoArrowAttr::Empty();
    midType.reset();
    skipType.reset();
    gvMidType.reset();
    gvSkipType.reset();
}


void FullArrowAttr::MakeComplete()
{
    TwoArrowAttr::MakeComplete();
    if (!midType && !gvMidType) { midType = EArrowType::SOLID; dirty = true; }
    if (type != EArcArrowType::BIGARROW)
        if (!skipType && !gvSkipType) { skipType = EArrowType::NONE; dirty = true; }
}

FullArrowAttr & FullArrowAttr::operator += (const FullArrowAttr &toadd)
{
    TwoArrowAttr::operator+=(toadd);
    //in all ernesty we strive to keep skipType NONE for bigarrows.
    ApplyArrowType(midType, toadd.midType, gvMidType, toadd.gvMidType, toadd.type);
    if (type != EArcArrowType::BIGARROW && toadd.type != EArcArrowType::BIGARROW)
        ApplyArrowType(skipType, toadd.skipType, gvSkipType, toadd.gvSkipType, toadd.type);
    return *this;
}

/** Take an Attribute object and add its value to us.
 * We recognize '*.skiptype' and '*.midtype' in addition to
 * TwoArrowAttr.*/
bool FullArrowAttr::AddAttribute(const Attribute &a, Chart *chart, EStyleType t)
{
    using Tup = std::tuple<bool, OptAttr<EArrowType>*, OptAttr<std::string>*>;
    if (type == EArcArrowType::BIGARROW && a.EndsWith("skiptype")) {
        chart->Error.Error(a, false, "Attribute 'skipType' is not applicable to block arrows. Ignoring it.");
        return true;
    }
    Tup pType = Tup(false, nullptr, nullptr), pType2 = pType;
    if (a.Is("arrow") || a.EndsWith("type") || a.EndsWith("endtype")) {
        pType = Tup(false, &endType, &gvEndType);
        pType2 = Tup(false, &midType, &gvMidType);
    } else if (a.EndsWith("gvtype") || a.EndsWith("gvendtype")) {
        pType = Tup(true, &endType, &gvEndType);
        pType2 = Tup(true, &midType, &gvMidType);
    } else if (a.EndsWith("midtype")) pType = Tup(false, &midType, &gvMidType);
    else if (a.EndsWith("gvmidtype")) pType = Tup(true, &midType, &gvMidType);
    else if (a.EndsWith("skiptype")) pType = Tup(false, &skipType, &gvSkipType);
    else if (a.EndsWith("gvskiptype")) pType = Tup(true, &skipType, &gvSkipType);
    //Note that starttype not handled here
    if (std::get<1>(pType)) {
        if (a.Is("arrow"))
            chart->Error.Warning(a, false, "Attribute 'arrow' is deprecated, but understood.",
                "Use 'arrow.type'.");
        if (std::get<0>(pType)) {
            if (SetArrowType(a, chart, t, *std::get<2>(pType))) {    //dirty is set if it returns true
                //graphviz style & successful parse
                std::get<1>(pType)->reset();
                if (std::get<2>(pType2)) *std::get<2>(pType2) = *std::get<2>(pType);
                if (std::get<1>(pType2)) *std::get<1>(pType2) = *std::get<1>(pType);
            }
        } else if (SetArrowType(a, chart, t, *std::get<1>(pType))) { //dirty is set if it returns true
            //old mscgen style & successful parse
            std::get<2>(pType)->reset();
            if (std::get<1>(pType2)) *std::get<1>(pType2) = *std::get<1>(pType);
            if (std::get<2>(pType2)) *std::get<2>(pType2) = *std::get<2>(pType);
        }
        return true;
    }
    return TwoArrowAttr::AddAttribute(a, chart, t);
}

void FullArrowAttr::AttributeNames(Csh &csh, std::string_view prefix)
{
    static const char * const names_descriptions[] ={
        "invalid", nullptr,
        "midtype", "The arrowhead type to use for multi-segment arrows, where the arrow stops an intermediate entity.",
        "skiptype", "The arrowhead type to use where the arrow goes over, but does not touch an entity.",
        "gvmidtype", "The arrowhead type to use (expressed in graphviz style) for multi-segment arrows, where the arrow stops an intermediate entity.",
        "gvskiptype", "The arrowhead type to use (expressed in graphviz style) where the arrow goes over, but does not touch an entity.",
        ""};
    csh.AddToHints(names_descriptions, csh.HintPrefix(COLOR_ATTRNAME).append(prefix), EHintType::ATTR_NAME);
    TwoArrowAttr::AttributeNames(csh, prefix);
}

bool FullArrowAttr::AttributeValues(std::string_view attr, Csh &csh, EArcArrowType t)
{
    if (CaseInsensitiveEndsWith(attr, "midtype") ||
        CaseInsensitiveEndsWith(attr, "skiptype"))
        return TwoArrowAttr::AttributeValues("endType", csh, t);
    if (CaseInsensitiveEndsWith(attr, "gvmidtype") ||
        CaseInsensitiveEndsWith(attr, "gvskiptype"))
        return TwoArrowAttr::AttributeValues("gvendType", csh, t);

    return TwoArrowAttr::AttributeValues(attr, csh, t);
}

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

bool WidthAttr::operator == (const WidthAttr &a) const noexcept
{
    if (a.is_set != is_set) return false;
    if (a.is_set && (a.value != value || (value<0 && a.str != str))) return false;
    return true;
}

/** Take an attribute and apply it to us.
*
* We consider attributes ending with 'width', 'pointer' and 'pos';
* or any style at the current context in `chart`. We also accept the clearing of
* an attribute if `t` is EStyleType::STYLE, that is for style definitions only.
* At a problem, we generate an error into chart->Error.
* @param [in] a The attribute to apply.
* @param chart The chart we build.
* @param [in] t The situation we set the attribute.
* @returns True, if the attribute was recognized as ours (may have been a bad value though).*/
bool WidthAttr::AddAttribute(const Attribute &a, Chart* chart, EStyleType t)
{
    if (a.EndsWith("width")) {
        switch (a.type) {
        default:
        case EAttrType::STYLE:
            _ASSERT(0);
            FALLTHROUGH;
        case EAttrType::CLEAR:
            if (a.EnsureNotClear(chart->Error, t))
                is_set = false;
            break;
        case EAttrType::NUMBER:
            is_set = true;
            value = a.number;
            str.clear();
            break;
        case EAttrType::BOOL:
        case EAttrType::STRING:
            is_set = true;
            value = -1;
            str = a.value;
            break;
        }
        return true;
    }
    return false;
}

/** Add a list of possible attribute value names to `csh` for attribute `attr`.*/
bool WidthAttr::AttributeValues(std::string_view attr, Csh &csh)
{
    if (CaseInsensitiveEndsWith(attr, "width")) {
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable() + "<number>",
            "Specify the width in pixels with a number.",
            EHintType::ATTR_VALUE, false));
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable() + "<string>",
            "You can also specify a string and then the width will be set to the width of this string.",
            EHintType::ATTR_VALUE, false));
        return true;
    }
    return false;
}

/** Get the width in pixels for the value of the width attribute.
 * Returns 0 if not set.*/
double WidthAttr::GetWidth(Canvas &canvas, const ShapeCollection &shapes, const StringFormat &format) const
{
    if (!is_set) return 0;
    if (value>=0) return value;
    if (str.length()==0) return 0;
    return value = Label(str, canvas, shapes, format).getTextWidthHeight().x;
}

/** Callback for drawing a symbol before 'yes' or 'no' in the hints popup list box.
 * @ingroup hintpopup_callbacks*/
bool CshHintGraphicCallbackForYesNo(Canvas *canvas, CshHintGraphicParam p, CshHintStore &)
{
    if (!canvas) return false;
    canvas->Clip(XY(1,1), XY(HINT_GRAPHIC_SIZE_X-1, HINT_GRAPHIC_SIZE_Y-1));
    cairo_t * const cr = canvas->GetContext();
    if (!cr) return false;
    cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
    cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);  //UnClip will remove these
    cairo_set_line_width(cr, 3);
    if (int(p)) {
        cairo_set_source_rgb(cr, 0, 0.8, 0); //green
        cairo_move_to(cr, HINT_GRAPHIC_SIZE_X*0.3, HINT_GRAPHIC_SIZE_Y*0.6);
        cairo_line_to(cr, HINT_GRAPHIC_SIZE_X*0.4, HINT_GRAPHIC_SIZE_Y*0.9);
        cairo_line_to(cr, HINT_GRAPHIC_SIZE_X*0.7, HINT_GRAPHIC_SIZE_Y*0.2);
    } else {
        cairo_set_source_rgb(cr, 1, 0, 0); //red
        const XY xy(HINT_GRAPHIC_SIZE_X/2, HINT_GRAPHIC_SIZE_Y/2);
        const double off = HINT_GRAPHIC_SIZE_Y*0.3;
        cairo_move_to(cr, xy.x-off, xy.y-off);
        cairo_line_to(cr, xy.x+off, xy.y+off);
        cairo_move_to(cr, xy.x-off, xy.y+off);
        cairo_line_to(cr, xy.x+off, xy.y-off);
    }
    cairo_stroke(cr);
    canvas->UnClip();
    return true;
}


