/*
    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_arrowhead.cpp The implementation of arrowhead styles and drawing including block arrows.
 * @ingroup libcgencommon_files */

 /** Define this, so Visual C defines constants, such as M_PI in math.h. */
#define _USE_MATH_DEFINES
#include <cmath>
#include <algorithm>
#include "cgen_arrowhead.h"
#include "canvas.h"
#include "chartbase.h"
#include "utf8utils.h"

constexpr double SingleArrowHead::baseArrowWidth;
constexpr double SingleArrowHead::baseArrowHeight;
constexpr double SingleArrowHead::baseDiamondSize;
constexpr double SingleArrowHead::baseDotSize;
constexpr double SingleArrowHead::baseBoxSize;
constexpr double SingleArrowHead::baseTeeWidth;
constexpr double SingleArrowHead::baseBlockSize;
constexpr double SingleArrowHead::SHARP_MUL_1;
constexpr double SingleArrowHead::SHARP_MUL_2;
constexpr double SingleArrowHead::VEE_MUL_1;
constexpr double SingleArrowHead::VEE_MUL_2;

constexpr bool FAST_ARROWHEAD = false;

/** Helper to decide if the arrowhead segment will have the arrow line covered after it,
 * and if so, the left or right side of the arrow line or both.
 * Essentially if we are last, the arrow line is not covered after us.
 * ICURVE and CROW does not cover the arrow line, so if we have only such after us
 * we will have nothing covered. If we have some elements covering only left or right
 * we return that, but if we have both after us, we have no line.*/
SingleArrowHead::EAfterMe SingleArrowHead::line_needed_after_me(bool flip, const SingleArrowHead * front, const SingleArrowHead * back) const
{
    if (this==back || back==nullptr || front==nullptr) return NONE_COVERED; //if we are last, the arrow line continues
    if (type==(this+1)->type && side==(this+1)->side &&
        (this+1)->are_we_part_of_a_half_line_series(front, back))
        return NONE_COVERED;
    //if we have ONLY DoesNotCoverArrowLineAtTip() type after us, we return a combination of left and right sides covered
    EAfterMe ret = NONE_COVERED;
    for (auto p = this+1; p-1!=back; p++)
        if (!DoesNotCoverArrowLineAtTip(p->type))
            switch (Flip(p->side, flip)) {
            default: _ASSERT(0); FALLTHROUGH;
            case ESingleArrowSide::BOTH: return ALL_COVERED;
            case ESingleArrowSide::LEFT: ret = EAfterMe(ret & LEFT_COVERED); break;
            case ESingleArrowSide::RIGHT: ret = EAfterMe(ret & RIGHT_COVERED); break;
            }
    return ret;
}
/** True, if we are a middle or a last element in a series of lLine, rLINE, lCURVE, rCURVE,
 * which series is the last in the arrowhead segments
 * or is only followed by DoesNotCoverArrowLineAtTip() types
 * Example rLINE, (rLINE) => true
 * Example rLINE, (rLINE), rLINE => true
 * Example (rLINE), rLINE => false: we are not in the middle
 * Example rLINE, (rLINE), CROW => true
 * Example rLINE, (rLINE), NORMAL => false: series followed by something that covers the line
 * Example LINE, LINE: false: not half*/
bool SingleArrowHead::are_we_part_of_a_half_line_series(const SingleArrowHead *front, const SingleArrowHead *back) const
{
    if (side==ESingleArrowSide::BOTH) return false;
    if (type!=ESingleArrowType::CURVE && type!=ESingleArrowType::LINE) return false;
    if (back && this!=back) {
        for (auto p = this+1; p-1!=back; p++)
            if (side!=p->side || type!=p->type) {
                if (p->line_needed_after_me(false, front, back)==NONE_COVERED) break;
                else return false;
            }
    }
    //ok, we are lLINE, rLINE, lCURVE, rCURVE and are followed by the same things
    //see if we are the first in the series. If so, return false.
    return front && this!=front && (this-1)->type==type && (this-1)->side==side;
}

/** Determines, how the arrow line needs to be clipped for a segment.
 * This depends on what segments come after us, what side are we on,
 * and what is our type and whether we ignore clipping for opaque
 * arrowheads.
 * We return a combination of SingleArrowHead::EClipLineType flags.*/
SingleArrowHead::EClipLineType
SingleArrowHead::clipline_type(bool flip, bool ignore_opaque,
                               const SingleArrowHead *front, const SingleArrowHead *back) const
{
    if (type==ESingleArrowType::NONE) return CLIP_NO;
    //For these we always have to cover the whole line (on our side)
    if (type==ESingleArrowType::NORMAL || type==ESingleArrowType::TEE || type==ESingleArrowType::TICK ||
        type==ESingleArrowType::BOX || type==ESingleArrowType::NSBOX) {
        if (ignore_opaque && !empty && type!=ESingleArrowType::NORMAL)
            return CLIP_NO;
        switch (Flip(side, flip)) {
        default: _ASSERT(0); FALLTHROUGH;
        case ESingleArrowSide::BOTH: return CLIP_ALL;
        case ESingleArrowSide::LEFT: return CLIP_LEFT;
        case ESingleArrowSide::RIGHT:return CLIP_RIGHT;
        }
    }
    //JUMPOVER always covers with its cover
    if (type==ESingleArrowType::JUMPOVER) return CLIP_ADD_COVER;
    const EAfterMe l_after = line_needed_after_me(flip, front, back);
    //for an icurve, if anybody covers anything after us, we clip all the line, else none
    if (type==ESingleArrowType::ICURVE) return l_after==NONE_COVERED ? CLIP_NO : CLIP_ALL;

    if (IsLine(type)) {
        _ASSERT(!IsSymmetricOrNone(type));
        //test if we are half and only the same half type is after us
        if (are_we_part_of_a_half_line_series(front, back)) return CLIP_NO;
        switch (l_after) {
        default: _ASSERT(0); FALLTHROUGH;
        case ALL_COVERED: return CLIP_ALL;
        case LEFT_COVERED: if (Flip(side, flip)==ESingleArrowSide::LEFT) return CLIP_LEFT;
                           else return CLIP_ALL;
        case RIGHT_COVERED: if (Flip(side, flip)==ESingleArrowSide::RIGHT) return CLIP_RIGHT;
                            else return CLIP_ALL;
        case NONE_COVERED:
            switch (Flip(side, flip)) {
            default: _ASSERT(0); FALLTHROUGH;
            case ESingleArrowSide::BOTH: return EClipLineType(CLIP_ALL + CLIP_MINUS_COVER);
            case ESingleArrowSide::LEFT: return EClipLineType(CLIP_LEFT + CLIP_MINUS_COVER);
            case ESingleArrowSide::RIGHT:return EClipLineType(CLIP_RIGHT + CLIP_MINUS_COVER);
            }
        }
    }
    //OK, here we have some of the body types: SHARP, INV, VEE, DOT, DIAMOND and NS versions
    switch (l_after) {
    default: _ASSERT(0); FALLTHROUGH;
    case ALL_COVERED: return CLIP_ALL;
    case LEFT_COVERED: if (Flip(side, flip)==ESingleArrowSide::LEFT) return CLIP_LEFT;
                       else return CLIP_ALL;
    case RIGHT_COVERED: if (Flip(side, flip)==ESingleArrowSide::RIGHT) return CLIP_RIGHT;
                        else return CLIP_ALL;
    case NONE_COVERED:
        const EClipLineType clip_cover = ignore_opaque && !empty ? CLIP_NO : CLIP_ADD_COVER;
        //if we are symmetric - return our cover
        if (IsSymmetricAndNotNone(type) && (front==nullptr || this==front))
            return CLIP_ADD_COVER;
        //for an crow, if anybody covers anything after us, we clip all the line, else our contour
        if (type==ESingleArrowType::CROW) return clip_cover;
        switch (Flip(side, flip)) {
        default: _ASSERT(0); FALLTHROUGH;
        case ESingleArrowSide::BOTH: return EClipLineType(CLIP_ALL + CLIP_ONLY_FRONT + clip_cover);
        case ESingleArrowSide::LEFT: return EClipLineType(CLIP_LEFT + CLIP_ONLY_FRONT + clip_cover);
        case ESingleArrowSide::RIGHT:return EClipLineType(CLIP_RIGHT + CLIP_ONLY_FRONT + clip_cover);
        }
    }
}



/** Tells how wide and tall one arrowhead is and how much offset is needed
 * to typeset the next one.
 * For symmetric arrowheads the width returned is the complete width, not
 * just the part on one side of the target (e.g., an entity line).
 * @param [in] mainline_before The line style of the arrow line from the direction
 *                             we point from (=before the target line). (Used by JUMPOVER type.)
 * @param [in] mainline_after The line style of the arrow line from the direction
 *                            we point to. (=after the target line) (Used by JUMPOVER type.)
 * @param [in] front A pointer to the first arrow in the series (may be ==this, if we are first)
 *                   (We assume elements of the arrow series come one after the other continuously in
 *                   memory, so that 'this-1' is the element before us (if this!=front && front!=nullptr)
 *                   It may be nullptr, if we are not part of an arrow series, but stand alone.
 *                  For line type arrowheads we return a smaller offset if the
 *                  next arrowhead is also of this type.
 * @param [in] back The iterator pointing to the last arrow in our series. It may be ==this, if
 *                  we are the last arrow. It may also ==front, if we are alone in the series.
 *                  It may be nullptr, if we are not part of an arrow series, but stand alone.
 * @returns The actual width (bounding box) of the arrowhead with half the height
 *          (that is, half_height equals how much the arrowhead spans above/below the arrow line),
 *          the x extents beyond the target line and before it (beyond is nonzero only for symmetric arrowheads)
 *          and the offset to be applied to the next arrowhead in the series.
 *          (Note that the offset is only from the target line, so if this is not the first in the series
 *           after_x_extents shall be added to it.*/
SingleArrowHead::WHORetval
SingleArrowHead::WidthHeightOffset(const LineAttr &mainline_before, const LineAttr &mainline_after,
                                   const SingleArrowHead *front, const SingleArrowHead *back) const
{
    _ASSERT(GoodForArrows(type));
    SingleArrowHead::WHORetval ret;
    switch (type) {
    default:
        _ASSERT(0);
        FALLTHROUGH;
    case ESingleArrowType::NONE:
        ret.before_x_extent = 0;
        ret.after_x_extent = 0;
        ret.half_height = 0;
        ret.offset = 0;
        break;

    case ESingleArrowType::NORMAL:
    case ESingleArrowType::INV:
        ret.before_x_extent = baseArrowWidth*scale.x + line.LineWidth();
        ret.after_x_extent = 0;
        ret.half_height = baseArrowHeight*scale.y + line.LineWidth();
        ret.offset = ret.before_x_extent;
        break;

    case ESingleArrowType::LINE: /* Two lines */
        ret.before_x_extent = baseArrowWidth*scale.x + line.LineWidth();
        ret.after_x_extent = 0;
        ret.half_height = baseArrowHeight*scale.y + line.LineWidth();
        ret.offset = (this!=back && front!=nullptr && back!=nullptr && IsTriangleShape((this+1)->type) ? 0.5 : 1.) * ret.before_x_extent;
        break;

    case ESingleArrowType::CURVE:
        ret.before_x_extent = baseDotSize*scale.x + line.LineWidth();
        ret.after_x_extent = 0;
        ret.half_height = baseDotSize*scale.y + line.LineWidth();
        ret.offset = (this!=back && front!=nullptr && back!=nullptr && (this+1)->type==ESingleArrowType::CURVE ? 0.5 : 1.) * ret.before_x_extent;
        break;

    case ESingleArrowType::ICURVE:
        ret.before_x_extent = baseDotSize*scale.x + line.LineWidth();
        ret.after_x_extent = 0;
        ret.half_height = baseDotSize*scale.y + line.LineWidth();
        ret.offset = (this!=back && front!=nullptr && back!=nullptr && (this+1)->type==ESingleArrowType::ICURVE ? 0.5 : 1.) * ret.before_x_extent;
        break;

    case ESingleArrowType::SHARP:
        ret.before_x_extent = baseArrowWidth*SHARP_MUL_1*scale.x + line.LineWidth();
        ret.after_x_extent = 0;
        ret.half_height = baseArrowHeight*scale.y + line.LineWidth();
        ret.offset = ret.before_x_extent;
        break;

    case ESingleArrowType::VEE:
    case ESingleArrowType::CROW:
        ret.before_x_extent = baseArrowWidth*VEE_MUL_1*scale.x + line.LineWidth();
        ret.after_x_extent = 0;
        ret.half_height = baseArrowHeight*scale.y + line.LineWidth();
        ret.offset = ret.before_x_extent;
        break;

    case ESingleArrowType::DIAMOND:
        if (this==front || front==nullptr) {
            ret.before_x_extent = ret.after_x_extent = baseDiamondSize*scale.x + line.LineWidth();
            ret.half_height = baseDiamondSize*scale.y + line.LineWidth();
            ret.offset = ret.before_x_extent;
            break;
        }
        //If we are not first, behave as nsdiamond...
        FALLTHROUGH;
    case ESingleArrowType::NSDIAMOND:
        ret.before_x_extent = 2*(baseDiamondSize*scale.x + line.LineWidth());
        ret.after_x_extent = 0;
        ret.half_height = baseDiamondSize*scale.y + line.LineWidth();
        ret.offset = ret.before_x_extent;
        break;

    case ESingleArrowType::DOT:
        if (this==front || front==nullptr) {
            ret.before_x_extent = ret.after_x_extent = baseDotSize*scale.x + line.LineWidth();
            ret.half_height = baseDotSize*scale.y + line.LineWidth();
            ret.offset = ret.before_x_extent;
            break;
        }
        //If we are not first, behave as nsdot...
        FALLTHROUGH;
    case ESingleArrowType::NSDOT:
        ret.before_x_extent = 2*(baseDotSize*scale.x + line.LineWidth());
        ret.after_x_extent = 0;
        ret.half_height = baseDotSize*scale.y + line.LineWidth();
        ret.offset = ret.before_x_extent;
        break;

    case ESingleArrowType::BOX:
        if (this==front || front==nullptr) {
            ret.before_x_extent = ret.after_x_extent = baseBoxSize*scale.x + line.LineWidth();
            ret.half_height = baseBoxSize*scale.y + line.LineWidth();
            ret.offset = ret.before_x_extent;
            break;
        }
        //If we are not first, behave as nsdot...
        FALLTHROUGH;
    case ESingleArrowType::NSBOX:
        ret.before_x_extent = 2*(baseBoxSize*scale.x + line.LineWidth());
        ret.after_x_extent = 0;
        ret.half_height = baseBoxSize*scale.y + line.LineWidth();
        ret.offset = ret.before_x_extent;
        break;

    case ESingleArrowType::TEE:
        ret.before_x_extent = 2*(baseTeeWidth*scale.x + line.LineWidth());
        ret.after_x_extent = 0;
        ret.half_height = baseDotSize*scale.y + line.LineWidth();
        ret.offset = ret.before_x_extent;
        break;

    case ESingleArrowType::TICK:
        ret.before_x_extent = line.LineWidth()/2;
        ret.after_x_extent = line.LineWidth()/2;
        ret.half_height = baseDotSize*scale.y + line.LineWidth();
        ret.offset = ret.before_x_extent;
        break;

    case ESingleArrowType::JUMPOVER:
        //midline will be at 'baseDotSize*0.66*scale + (mainlinewidth_bef+mainlinewidth_aft)/2'
        const double lw2b = mainline_before.LineWidth()/2;
        const double lw2a = mainline_after.LineWidth()/2;
        const double m = std::max(lw2a, lw2b);
        ret.before_x_extent = baseDotSize*0.66*scale.x + m + lw2b;
        ret.after_x_extent = baseDotSize*0.66*scale.x + m + lw2a;
        ret.half_height = baseDotSize*0.66*scale.y + 2*m;
        ret.offset = ret.before_x_extent;
        break;
    }
    return ret;
}
/** Helper that creates a diamond shape.
 * @param xy The center of the diamond.
 * @param wh */
Contour diamond(XY xy, XY wh)
{
    wh.x = fabs(wh.x);
    wh.y = fabs(wh.y);
    XY points[] = {XY(xy.x,      xy.y-wh.y),
                   XY(xy.x+wh.x, xy.y     ),
                   XY(xy.x,      xy.y+wh.y),
                   XY(xy.x-wh.x, xy.y     )};
    return Contour(std::span<XY>(points));
}


/** Returns an area where the arrow line should not be drawn.
 * The inverse of it will be used to clip, when drawing the arrow line.
 * Note that we have to consider lines that are thicker than the arrowhead.
 * We assume the arrowhead points left to right along a straight line.
 * @param [in] xy The tip of the arrowhead - always the middle of the entity line even if the entity is activated
 * @param [in] flip If true, the arrowhead needs to be flipped to the arrow line.
 *                  useful for the 'half' style, which is not symmetric to the arrow line.
 * @param [in] ignore_opaque When set to true, we will not clip the line for filled arrowheads.
 *                           This may lead to better performance if you draw the arrowhead
 *                           *after* the arrow line and the fill color is opaque.
 * @param [in] mainline_before The line style of the arrow line from the direction
 *                             we point from (=before the target line). (Used by JUMPOVER type.)
 * @param [in] mainline_after The line style of the arrow line from the direction
 *                            we point to. (=after the target line) (Used by JUMPOVER type.)
 * @param [in] front A pointer to the first arrow in the series (may be ==this, if we are first)
 *                   (We assume elements of the arrow series come one after the other continuously in
 *                   memory, so that 'this-1' is the element before us (if this!=front && front!=nullptr)
 *                   It may be nullptr, if we are not part of an arrow series, but stand alone.
 *                   For half type type arrowheads we don't clip if we are after a half
 *                   arrowhead ourselves.
 * @param [in] back The iterator pointing to the last arrow in our series. It may be ==this, if
 *                  we are the last arrow. It may also ==front, if we are alone in the series.
 *                  It may be nullptr, if we are not part of an arrow series, but stand alone.
 * @returns An area covering the area where the arrow line should NOT be drawn.
 *          If no clipping is needed (all of the line can be shown at this arrowhead),
 *          we return an empty contour.*/
Contour SingleArrowHead::ClipLine(const XY & xy, bool flip, bool ignore_opaque,
                                  const LineAttr & mainline_before, const LineAttr & mainline_after,
                                  const SingleArrowHead *front, const SingleArrowHead *back) const
{
    _ASSERT(GoodForArrows(type));
    if (!line.color->IsFullyOpaque()) ignore_opaque = false;
    const EClipLineType t = clipline_type(flip, ignore_opaque, front, back);
    Contour ret;
    if (t==CLIP_NO) return ret;
    auto who = WidthHeightOffset(mainline_before, mainline_after, front, back);
    const double lw2_1 = std::max(ceil(mainline_before.LineWidth()/2), who.half_height);
    const double lw2_2 = std::max(ceil(mainline_after.LineWidth()/2), who.half_height);
    const double l = std::max(lw2_1, lw2_2);
    Block b;
    if (t & CLIP_ONLY_FRONT) b.x = {xy.x-who.offset/2, xy.x};
    else b.x = {xy.x-who.offset, xy.x+who.after_x_extent};
    switch (EClipLineType(t & 3)) {
    default: break;
    case CLIP_LEFT: b.y = {xy.y-l, xy.y}; ret = b; break;
    case CLIP_RIGHT: b.y = {xy.y, xy.y+l}; ret = b; break;
    case CLIP_ALL: b.y = {xy.y-l, xy.y+l}; ret = b; break;
    }
    if (t & CLIP_ADD_COVER) ret += Cover(xy, flip, mainline_before, mainline_after, front, back);
    else if (t & CLIP_MINUS_COVER) ret -= Cover(xy, flip, mainline_before, mainline_after, front, back);
    return ret;
}

/** Returns a contour covering one arrowhead pointing left to right along a straight line.
 * @param [in] xy The tip of the arrowhead. The arrow points from left to right (towards x axis infinity)
 * @param [in] flip If true, the arrowhead needs to be flipped to the arrow line.
 *                  useful for the 'half' style, which is not symmetric to the arrow line.
 * @param [in] mainline_before The line style left of the arrowhead
 * @param [in] mainline_after The line style right of the arrowhead
 * @param [in] front A pointer to the first arrow in the series (may be ==this, if we are first)
 *                   (We assume elements of the arrow series come one after the other continuously in
 *                   memory, so that 'this-1' is the element before us (if this!=front && front!=nullptr)
 *                   It may be nullptr, if we are not part of an arrow series, but stand alone.
 * @param [in] back The iterator pointing to the last arrow in our series. It may be ==this, if
 *                  we are the last arrow. It may also ==front, if we are alone in the series.
 *                  It may be nullptr, if we are not part of an arrow series, but stand alone.
 * @returns the contour of the arrowhead. For line type arrowheads some edges may be set to invisible:
 *          the ones you do not need to draw.*/
Contour SingleArrowHead::Cover(const XY & xy, bool flip,
                               const LineAttr & mainline_before, const LineAttr & mainline_after,
                               const SingleArrowHead *front, const SingleArrowHead *back) const
{
    _ASSERT(GoodForArrows(type));

    const auto who = WidthHeightOffset(mainline_before, mainline_after, front, back);
    Contour tri1(xy - XY( who.before_x_extent, who.half_height),
                 xy,
                 xy + XY(-who.before_x_extent, who.half_height));
    Contour area;
    switch(type) {
    case ESingleArrowType::NONE:
        break;

    case ESingleArrowType::NORMAL:
        area = std::move(tri1);
        break;

    case ESingleArrowType::LINE: /* Two lines */
        area = std::move(tri1);
        //We draw lines, omit the vertical part
        area.front().Outline()[2].SetVisible(false);
        break;

    case ESingleArrowType::SHARP:
        area = std::move(tri1) - Contour(xy - XY( who.before_x_extent, who.half_height),
                                         xy - XY(who.before_x_extent*SHARP_MUL_2, 0),
                                         xy + XY(-who.before_x_extent, who.half_height));
        break;

    case ESingleArrowType::VEE:
        area = std::move(tri1) - Contour(xy - XY( who.before_x_extent, who.half_height),
                                         xy - XY(who.before_x_extent*VEE_MUL_2, 0),
                                         xy + XY(-who.before_x_extent, who.half_height));
        break;

    case ESingleArrowType::CROW:
        area = Contour(xy - XY(0, who.half_height),
                       xy - XY(who.before_x_extent, 0),
                       xy + XY(0, who.half_height));
        area -= Contour(xy - XY(0, who.half_height),
                        xy - XY(who.before_x_extent*(1-VEE_MUL_2), 0),
                        xy + XY(0, who.half_height));
        break;

    case ESingleArrowType::DIAMOND:
        if (this==front || front==nullptr) {
            area = diamond(xy, XY(who.before_x_extent, who.half_height));
            break;
        }
        //If we are not first, behave as nsdiamond...
        FALLTHROUGH;
    case ESingleArrowType::NSDIAMOND:
        area = diamond(xy-XY(who.before_x_extent/2, 0), XY(who.before_x_extent/2, who.half_height));
        break;


    case ESingleArrowType::DOT:
        if (this==front || front==nullptr) {
            area = Contour(xy, who.before_x_extent, who.half_height);
            break;
        }
        //If we are not first, behave as nsdot...
        FALLTHROUGH;
    case ESingleArrowType::NSDOT:
        area = Contour(xy-XY(who.before_x_extent/2, 0), who.before_x_extent/2, who.half_height);
        break;

    case ESingleArrowType::BOX:
    case ESingleArrowType::NSBOX:
    case ESingleArrowType::TEE:
    case ESingleArrowType::TICK:
        area = Block(xy-XY(who.before_x_extent, who.half_height), xy+XY(who.after_x_extent, who.half_height));
        break;

    case ESingleArrowType::INV:
        area = Contour(xy - XY(0, who.half_height),
                       xy - XY(who.before_x_extent, 0),
                       xy + XY(0, who.half_height));
        break;

    case ESingleArrowType::CURVE:
        area = Contour(xy-XY(who.before_x_extent, 0), who.before_x_extent, who.half_height);
        //area -= Contour(xy-XY(who.before_x_extent, 0), who.before_x_extent-line.LineWidth(), who.half_height-line.LineWidth());
        area -= Contour(Block(xy-XY(who.before_x_extent*2, who.half_height), xy-XY(who.before_x_extent, -who.half_height))).SetVisible(false);
        break;

    case ESingleArrowType::ICURVE:
        area = Contour(xy, who.before_x_extent, who.half_height);
        //area -= Contour(xy, who.before_x_extent-line.LineWidth(), who.half_height-line.LineWidth());
        area -= Contour(Block(xy-XY(0, who.half_height), xy+XY(who.before_x_extent, who.half_height))).SetVisible(false);
        break;

    case ESingleArrowType::JUMPOVER:
        if (mainline_after.LineWidth() == mainline_before.LineWidth()) {
            double lw2 = mainline_after.LineWidth()/2;
            area = Contour(xy, who.before_x_extent, who.half_height) *
                Contour(xy.x-who.before_x_extent*2, xy.x+who.after_x_extent*2,
                        xy.y-who.half_height*2, xy.y+lw2);
        } else {
            const double lw2b = mainline_before.LineWidth()/2;
            const double lw2a = mainline_after.LineWidth()/2;
            const double m = std::max(lw2b, lw2a);
            area = Contour(xy, who.before_x_extent, who.half_height-m+lw2b) *
                Contour(xy.x-who.before_x_extent*2, xy.x,
                        xy.y-who.half_height*2, xy.y+lw2b);
            area += Contour(xy, who.after_x_extent, who.half_height-m+lw2a) *
                 Contour(xy.x, xy.x+who.after_x_extent*2,
                         xy.y-who.half_height*2, xy.y+lw2a);
        }
        break;

    default:
        _ASSERT(0);
    }
    //cut cover if we are half only
    if (side!=ESingleArrowSide::BOTH && !area.IsEmpty() && CanBeHalf(type)) {
        const Range y = Flip(side, flip)==ESingleArrowSide::LEFT ?
                           Range(xy.y, area.GetBoundingBox().y.till+1) :
                           Range(area.GetBoundingBox().y.from-1, xy.y);
        const Contour take_out = Block(area.GetBoundingBox().x.CreateExpanded(1), y);
        if (IsLine(type))
            take_out.SetVisible(false);
        area -= take_out;
    }
    return area;
}

/** Draws the arrowhead pointing left to right along a straight line.
 * @param canvas The canvas to draw onto.
 *               If the canvas is not doing graphics, but shape output (e.g., PPT),
 *               we emit the necessary shapes - even though for such output we prefer
 *               using the arrowhead attribute of lines to actually emitting shapes
 *               for the arrowhead.
 * @param [in] xy The tip of the arrowhead. The arrow points from left to right (towards x axis infinity)
 * @param [in] flip If true, the arrowhead needs to be flipped to the arrow line.
 *                  useful for the 'half' style, which is not symmetric to the arrow line.
 * @param [in] mainline_before The line style left of the arrowhead
 * @param [in] mainline_after The line style right of the arrowhead
 * @param [in] front A pointer to the first arrow in the series (may be ==this, if we are first)
 *                   (We assume elements of the arrow series come one after the other continuously in
 *                   memory, so that 'this-1' is the element before us (if this!=front && front!=nullptr)
 *                   It may be nullptr, if we are not part of an arrow series, but stand alone.
 * @param [in] back The iterator pointing to the last arrow in our series. It may be ==this, if
 *                  we are the last arrow. It may also ==front, if we are alone in the series.
 *                  It may be nullptr, if we are not part of an arrow series, but stand alone.*/
void SingleArrowHead::Draw(Canvas & canvas, const XY & xy, bool flip,
                           const LineAttr & mainline_before, const LineAttr & mainline_after,
                           const SingleArrowHead *front, const SingleArrowHead *back) const
{
    _ASSERT(GoodForArrows(type));
    _ASSERT(line.IsComplete());
    if (type == ESingleArrowType::NONE) return;

    //Do jumpover - oblivious to 'side' or 'empty'
    if (type == ESingleArrowType::JUMPOVER) {
        const auto who = WidthHeightOffset(mainline_before, mainline_after, front, back);
        const double lw2b = mainline_before.LineWidth()/2;
        const double lw2a = mainline_after.LineWidth()/2;
        const double m = std::max(lw2b, lw2a);
        Path p = {Edge(XY(xy.x-who.before_x_extent*2-m, xy.y), XY(xy.x-who.before_x_extent+lw2b, xy.y))};
        p.AppendEllipse(xy, who.before_x_extent-lw2b, who.half_height-m, 0, 180, 360);
        p.emplace_back(XY(xy.x+who.after_x_extent-lw2a, xy.y), XY(xy.x+who.after_x_extent*2+m, xy.y));
        if (mainline_before == mainline_after) {
            const Contour circle(xy, who.before_x_extent, who.half_height);
            if (canvas.does_graphics()) {
                canvas.Clip(circle);
                canvas.Line(p, mainline_before);
                canvas.UnClip();
            } else {
                p.ClipRemove(circle);
                canvas.Add(GSPath(std::move(p), mainline_before));
            }
        } else {
            Block side(xy.x-who.before_x_extent*2, xy.x, xy.y, xy.y);
            side.y.Expand(who.half_height*2);
            const Contour circle1 = Contour(xy, who.before_x_extent, who.half_height-m+lw2b)* side;
            side.x = { xy.x, xy.x+who.after_x_extent*2 };
            const Contour circle2 = Contour(xy, who.after_x_extent, who.half_height-m+lw2a) * side;
            if (canvas.does_graphics()) {
                canvas.Clip(circle1);
                canvas.Line(p, mainline_before);
                canvas.UnClip();
                canvas.Clip(circle2);
                canvas.Line(p, mainline_after);
                canvas.UnClip();
            } else {
                Path p2 = p;
                p.ClipRemove(circle1);
                p2.ClipRemove(circle2);
                canvas.Add(GSPath(std::move(p), mainline_before));
                canvas.Add(GSPath(std::move(p2), mainline_after));
            }
        }
        return;
    }

    Contour cover;
    Block clip(false);
    if (side!=ESingleArrowSide::BOTH && type==ESingleArrowType::LINE) {
        //for half side lines, we need the contour of the full '>' line arrowhead to draw nicely
        //All other half arrowheads are OK with their normal cover
        const auto who = WidthHeightOffset(mainline_before, mainline_after, front, back);
        cover = Contour (xy - XY( who.before_x_extent, who.half_height),
                         xy,
                         xy + XY(-who.before_x_extent, who.half_height));
        cover.front().Outline()[2].SetVisible(false);
        //clip for half
        clip = cover.GetBoundingBox().CreateExpand(1);
        if (this==front || front==nullptr || ((this-1)->side==ESingleArrowSide::BOTH && IsLine((this-1)->type))) {
            if (Flip(side, flip)==ESingleArrowSide::LEFT)
                clip.y.till = xy.y;
            else
                clip.y.from = xy.y;
        } else {
            //for double or triple arrows, second and third line should be shorter, so that
            //they do not extend to the middle of a double line
            if (Flip(side, flip)==ESingleArrowSide::LEFT)
                clip.y.till = xy.y - mainline_before.Spacing();
            else
                clip.y.from = xy.y + mainline_before.Spacing();
        }
    } else
        cover = Cover(xy, flip, mainline_before, mainline_after, front, back); //has invisible edges for the line types
    if (IsLine(type) || empty) {
        //Shrink by linewidth so that the above contours are outer edge
        if (!canvas.HasImprecisePositioning())
            cover.Expand(-line.LineWidth()/2);
        if (canvas.does_graphics()) {
            const cairo_line_cap_t old = canvas.SetLineCap(CAIRO_LINE_CAP_ROUND);
            if (!clip.IsInvalid()) canvas.Clip(clip);
            canvas.Line(cover, line);
            if (!clip.IsInvalid()) canvas.UnClip();
            canvas.SetLineCap(old);
        } else {
            Path path = cover;
            if (!clip.IsInvalid()) path.ClipRemove(clip);
            canvas.Add(GSPath(std::move(path), line));
        }
    } else { //fill
        _ASSERT(clip.IsInvalid());
        if (canvas.does_graphics())
            canvas.Fill(cover, FillAttr::Solid(*line.color));
        else
            canvas.Add(GSShape(cover, LineAttr::None(), FillAttr::Solid(*line.color)));
    }
}

Contour SingleArrowHead::curvy_line_cover(const Path & path, const PathPos & pos, bool forward, bool flip,
                                          unsigned num, double * gap1, double * gap2, double * length) const
{
    Edge::Update update1, update2;
    double gap_replacement[10];
    if (side!=ESingleArrowSide::BOTH) {
        //zero out the respective side if we are half
        //if gap1==gap2, create a copy
        _ASSERT(num+1<10);
        if (gap1==gap2) {
            gap2 = gap_replacement;
            for (unsigned u = 0; u<=num; u++)
                gap2[u] = gap1[u];
        }
        double *zero = flip == (side==ESingleArrowSide::LEFT) ? gap2 : gap1;
        for (unsigned u = 0; u<=num; u++)
            zero[u] = 0;
        //if we are a line-type, mark the zeroed side as invisible
        if (IsLine(type))
            (flip == (side==ESingleArrowSide::LEFT) ? update2 : update1) = Edge::Update::clear_visible();
    }
    if (forward)
        for (unsigned u = 0; u<num; u++)
            length[u] *= -1;
    return path.LinearWidenAsymetric(pos, num, gap1, gap2, length, update1, update2, false);
}

template<typename ...Doubles>
double *D(double *p, double v, Doubles ... doubles) { *p = v; D(p+1, doubles...); return p; }

template<>
double *D(double *p, double v) { *p = v; return p; }


/** Returns the area covered by the arrowhead when drawn onto a (potentially curvy and bendy) path.
 * @param [in] path The path onto which the arrowhead is drawn.
 * @param [in] pos The position where the tip of the arrowhead shall be on the path.
 * @param [in] forward If true the arrowhead points towards the end of the path (and is thus fully *before* 'pos').
 * @param [in] flip If true, the arrowhead needs to be flipped to the arrow line.
 *                  useful for the 'half' style, which is not symmetric to the arrow line.
 * @param [in] mainline_before The line style of the path before the tip (as seen from the arrowhead's perspective).
 *                             If 'forward' is false, this is actually the line style from tip towards the end of the path.
 * @param [in] mainline_after The line style of the path after the tip (as seen from the arrowhead's perspective).
 * @param [in] front A pointer to the first arrow in the series (may be ==this, if we are first)
 *                   (We assume elements of the arrow series come one after the other continuously in
 *                   memory, so that 'this-1' is the element before us (if this!=front && front!=nullptr)
 *                   It may be nullptr, if we are not part of an arrow series, but stand alone.
 * @param [in] back The iterator pointing to the last arrow in our series. It may be ==this, if
 *                  we are the last arrow. It may also ==front, if we are alone in the series.
 *                  It may be nullptr, if we are not part of an arrow series, but stand alone.
 * @returns the area covered by the arrowhead. For some of the arrowheads, some edges will be set to invisible - those
 *          that don't have actually to be drawn (e.g., for line, half and jumpover). You may want to call SetVisible(true)
 *          on the result if you want to display this contour.*/
Contour SingleArrowHead::Cover(const Path & path, const PathPos & pos, bool forward, bool flip,
                               const LineAttr &mainline_before, const LineAttr & mainline_after,
                               const SingleArrowHead *front, const SingleArrowHead *back) const
{
    _ASSERT(GoodForArrows(type));

    const auto who = WidthHeightOffset(mainline_before, mainline_after, front, back);
    const double sign = forward ? -1 : +1; //for forward looking arrows we step backwards.
    Contour area;
    PathPos pos2 = pos;
    double gap[5], length[4];

    switch (type) {
    case ESingleArrowType::NONE:
        break;
    case ESingleArrowType::NORMAL:
    case ESingleArrowType::LINE:
        area = curvy_line_cover(path, pos, forward, flip, 1, D(gap, 0, who.half_height), gap, D(length, who.before_x_extent));
        break;

    case ESingleArrowType::INV:
        area = curvy_line_cover(path, pos, forward, flip, 1, D(gap, who.half_height, 0), gap, D(length, who.before_x_extent));
        break;

    case ESingleArrowType::SHARP:
        area = curvy_line_cover(path, pos, forward, flip, 1, D(gap, 0, who.half_height), gap, D(length, who.before_x_extent));
        if (0==path.MovePos(pos2, sign*who.before_x_extent*SHARP_MUL_2))
            //area to substract - make this 10% longer and wider to be surely larger than the first component
            area -= curvy_line_cover(path, pos2, forward, flip, 1, D(gap, 0, who.half_height*1.1), gap, D(length, who.before_x_extent*(1-SHARP_MUL_2)*1.1));
        break;

    case ESingleArrowType::VEE:
        area = curvy_line_cover(path, pos, forward, flip, 1, D(gap, 0, who.half_height), gap, D(length, who.before_x_extent));
        if (0==path.MovePos(pos2, sign*who.before_x_extent*VEE_MUL_2))
            //area to substract - make this 10% longer and wider to be surely larger than the first component
            area -= curvy_line_cover(path, pos2, forward, flip, 1, D(gap, 0, who.half_height*1.1), gap, D(length, who.before_x_extent*(1-VEE_MUL_2)*1.1));
        break;

    case ESingleArrowType::CROW:
        area = curvy_line_cover(path, pos, forward, flip, 1, D(gap, who.half_height, 0), gap, D(length, who.before_x_extent));
        if (0==path.MovePos(pos2, sign*who.before_x_extent*(1-VEE_MUL_2)*-0.1))
            //area to substract - make this 10% longer and wider to be surely larger than the first component
            area -= curvy_line_cover(path, pos2, forward, flip, 1, D(gap, who.half_height*1.1, 0), gap, D(length, who.before_x_extent*(1-VEE_MUL_2)*1.1));
        break;

    case ESingleArrowType::DIAMOND:
        if (this==front || front==nullptr) {
            Path p(path);
            p.LinearExtend(pos2, who.before_x_extent, forward);
            area = curvy_line_cover(p, pos2, forward, flip, 2, D(gap, 0, who.half_height, 0), gap, D(length, who.before_x_extent, who.after_x_extent));
            break;
        }
        //If we are not first, behave as nsdiamond...
        FALLTHROUGH;
    case ESingleArrowType::NSDIAMOND:
        area = curvy_line_cover(path, pos, forward, flip, 2, D(gap, 0, who.half_height, 0), gap, D(length, who.before_x_extent/2, who.before_x_extent/2));
        break;

    case ESingleArrowType::BOX:
        if (this==front || front==nullptr) {
            Path p(path);
            p.LinearExtend(pos2, who.before_x_extent, forward);
            area = curvy_line_cover(p, pos2, forward, flip, 1, D(gap, who.half_height, who.half_height), gap, D(length, who.before_x_extent+who.after_x_extent));
            break;
        }
        FALLTHROUGH;
    case ESingleArrowType::NSBOX:
    case ESingleArrowType::TEE:
        area = curvy_line_cover(path, pos, forward, flip, 1, D(gap, who.half_height, who.half_height), gap, D(length, who.before_x_extent));
        break;

    case ESingleArrowType::TICK: {
        //just a line perpendicular to the arrow line at 'pos'
        XY tip = path.GetPoint(pos);
        XY delta_parallel = (path.NextTangentPoint(pos, false)-tip).Normalize();
        XY delta_perpendicular = delta_parallel.Rotate90CCW()*who.half_height;
        delta_parallel *= who.before_x_extent;
        area = Contour({tip-delta_parallel-delta_perpendicular, tip-delta_parallel+delta_perpendicular,
                        tip+delta_parallel+delta_perpendicular, tip+delta_parallel-delta_perpendicular});
        break;
    }

    case ESingleArrowType::DOT:
    case ESingleArrowType::NSDOT:
    case ESingleArrowType::CURVE:
    case ESingleArrowType::ICURVE: {
        //dots are drawn with the canonical radius, thus on curvy paths
        //the edge of the dot may be further along the path than the
        //canonical radius measured on the path itself. Thus any
        //subsequent arrowhead in an arrow series will partially cover us.
        //Well, back luck.
        double half_width = who.before_x_extent;
        PathPos pos_copy(pos);
        switch (type) {
        default: _ASSERT(0); FALLTHROUGH;
        case ESingleArrowType::DOT:
            if (this==front || front==nullptr) break;
            FALLTHROUGH;
        case ESingleArrowType::NSDOT:
            //move tip half length backwards if we are not the first or an nsdot
            //to get the center. In these case raius is also half of who.before_x_extent
            half_width /= 2;
            FALLTHROUGH;
        case ESingleArrowType::CURVE:
            path.MovePos(pos_copy, sign*half_width);
            break;
        case ESingleArrowType::ICURVE:
            //do nothing, center=tip and radius is OK
            break;
        }
        const XY tip = path.GetPoint(pos_copy);
        const XY t1 = path.GetTangent(pos_copy, !forward, false);
        const XY t2 = path.GetTangent(pos_copy, forward, false);

        switch (type) {
        default: _ASSERT(0); FALLTHROUGH;
        case ESingleArrowType::DOT:
        case ESingleArrowType::NSDOT: {
            area = Contour(tip, half_width, who.half_height, GetDegree(tip, t1), -90, +90);
            Contour second(tip, half_width, who.half_height, GetDegree(tip, t2), -90, +90);
            //remove tips and add fill
            if (contour::triangle_dir(t1, tip, t2)!=contour::ETriangleDirType::IN_LINE) {
                Contour tmp = area^second;
                auto main = tmp.FindSimpleContour(tip, false);
                area *= second;
                for (auto p : main)
                    area += Contour(std::move(*p));
                //add fill
            } else {
                area += second;
            }
            break;
        }
        case ESingleArrowType::CURVE:
            area = Contour(tip, half_width, who.half_height, GetDegree(tip, t2), -90, +90);
            //make the line invisible - it is always the last one
            if (!area.IsEmpty())
                area.front().Outline().back().SetVisible(false);
            break;
        case ESingleArrowType::ICURVE:
            area = Contour(tip, half_width, who.half_height, GetDegree(tip, t1), -90, +90);
            //make the line invisible - it is always the last one
            if (!area.IsEmpty())
                area.front().Outline().back().SetVisible(false);
            break;
        }

        //OK, we need to cut them in halves
        if (side!=ESingleArrowSide::BOTH) {
            //make a longer path that surely starts/ends outside the circle 'area'
            Path path2(path);
            path2.LinearExtend(half_width*2, !forward);
            path2.LinearExtend(half_width*2, forward);
            if (Flip(side, flip ^ forward)==ESingleArrowSide::LEFT)
                path2.Invert();
            area *= path2.SimpleWidenAsymmetric(who.half_height*2, 0).SetVisible(!IsLine(type));
        }
        break;
    }

    case ESingleArrowType::JUMPOVER: {
        const XY xy = path.GetPoint(pos);
        //we always do it on the left side of the path
        //make a longer path that surely starts/ends outside the circle 'area'
        Path path2(path);
        path2.LinearExtend(who.before_x_extent*2, !forward);
        path2.LinearExtend(who.after_x_extent*2, forward);
        if (mainline_before.LineWidth()==mainline_after.LineWidth()) {
            _ASSERT(who.before_x_extent==who.after_x_extent);
            area = Contour(xy, who.before_x_extent, who.half_height);
            area *= path2.SimpleWidenAsymmetric(who.half_height*2, -mainline_before.LineWidth()/2).SetVisible(false);
        } else {
            area = Contour(xy, who.before_x_extent) *
                path2.SimpleWidenAsymmetric(who.half_height*2, -mainline_before.LineWidth()/2).SetVisible(false).SetInternalMark(false);
            //determine the point where we switch from mainline_before to mainline_after
            XY dir = area.Centroid() - xy;
            if (dir.length()<0.0001) break;//full circle or very small, we are done.
            dir.Normalize() *= std::max(who.before_x_extent, who.after_x_extent)*2;
            const XY block1[4] = {xy-dir, xy-dir + dir.Rotate90CCW(), xy+dir + dir.Rotate90CCW(), xy+dir};
            area *= Contour(std::span<const XY>(block1)).SetVisible(false).SetInternalMark(true);
            //OK, now add other side
            const XY block2[4] = {xy-dir, xy-dir + dir.Rotate90CW(), xy+dir + dir.Rotate90CW(), xy+dir};
            area += Contour(xy, who.after_x_extent) *
                path2.SimpleWidenAsymmetric(who.half_height*2, -mainline_after.LineWidth()/2).SetVisible(false).SetInternalMark(false) *
                Contour(std::span<const XY>(block2)).SetVisible(false).SetInternalMark(true);
            //the marked edge will be where we switch from the smaller to the larger linewidth
            //Mark the edge that is towards the circle, whose distance is closest to
            //(who.before_x_extent + who.after_x_extent)/2.
            const double target = (who.before_x_extent + who.after_x_extent)/2;
            double dist = DBL_MAX;
            const Edge *mark = nullptr;
            for (const Edge &e : area[0].Outline().GetEdges())
                if (e.IsInternalMarked()) {
                    e.SetInternalMark(false);
                    if (dist > fabs(e.Pos2Point(0.5).Distance(xy) - target)) {
                        dist = fabs(e.Pos2Point(0.5).Distance(xy) - target);
                        mark = &e;
                    }
                }
            _ASSERT(dist < 0.01);
            if (mark)
                mark->SetInternalMark(true);
            _ASSERT(area.CreatePathFromSelected([](const Edge &e) {return e.IsInternalMarked(); }).size()==1);
        }
        break;
    }
    default:
    case ESingleArrowType::STRIPES:
    case ESingleArrowType::TRIANGLE_STRIPES:
        _ASSERT(0);
        break;
    }
    if (!IsLine(type) && type!=ESingleArrowType::JUMPOVER)
        area.SetVisible(true);

    return area;
}

/** Returns an area where the arrow line should not be drawn.
 * The inverse of it will be used to clip, when drawing the arrow line.
 * Note that we have to consider lines that are thicker than the arrowhead.
 * @param [in] path The path onto which the arrowhead is drawn.
 * @param [in] pos The position where the tip of the arrowhead shall be on the path.
 * @param [in] forward If true the arrowhead points towards the end of the path (and is thus fully *before* 'pos').
 * @param [in] flip If true, the arrowhead needs to be flipped to the arrow line.
 *                  useful for the 'half' style, which is not symmetric to the arrow line.
 * @param [in] ignore_opaque When set to true, we will not clip the line for filled arrowheads.
 *                           This may lead to better performance if you draw the arrowhead
 *                           *after* the arrow line and the fill color is opaque.
 * @param [in] mainline_before The line style of the path before the tip (as seen from the arrowhead's perspective).
 *                             If 'forward' is false, this is actually the line style from tip towards the end of the path.
 * @param [in] mainline_after The line style of the path after the tip (as seen from the arrowhead's perspective).
 * @param [in] front A pointer to the first arrow in the series (may be ==this, if we are first)
 *                   (We assume elements of the arrow series come one after the other continuously in
 *                   memory, so that 'this-1' is the element before us (if this!=front && front!=nullptr)
 *                   It may be nullptr, if we are not part of an arrow series, but stand alone.
 * @param [in] back The iterator pointing to the last arrow in our series. It may be ==this, if
 *                  we are the last arrow. It may also ==front, if we are alone in the series.
 *                  It may be nullptr, if we are not part of an arrow series, but stand alone.
 * @param [in] cached_cover A pointer pointing to a contour holding the result of a previous call to Cover()
 *                          with the *exact same parameters* as to this call; or nullptr if no such contour is
 *                          available. (Then we call it ourselves.) This is to speed up processing.
 * @returns the area to omit from the arrow line.*/
Contour SingleArrowHead::ClipLine(const Path & path, const PathPos & pos, bool forward, bool flip, bool ignore_opaque,
                                  const LineAttr & mainline_before, const LineAttr & mainline_after,
                                  const SingleArrowHead *front, const SingleArrowHead *back,
                                  const Contour *cached_cover) const
{
    _ASSERT(GoodForArrows(type));
    if (!line.color->IsFullyOpaque()) ignore_opaque = false;
    const EClipLineType t = clipline_type(flip, ignore_opaque, front, back);
    Contour ret;
    if (t==CLIP_NO) return ret;
    auto who = WidthHeightOffset(mainline_before, mainline_after, front, back);
    const double lw = std::max(ceil(mainline_before.LineWidth()/2), ceil(mainline_after.LineWidth()/2))+0.5;
    PathPos p1 = pos, p2 = pos;
    const int dir = forward ? -1 : +1;
    if (t & CLIP_ONLY_FRONT) {
        path.MovePos(p1, dir*who.offset/2);
    } else {
        path.MovePos(p1, dir*who.offset);
        path.MovePos(p2, -dir*who.after_x_extent);
    }
    Path path2 = path.CopyPart(p1, p2);
    if (side==ESingleArrowSide::BOTH) {
        ret = path2.SimpleWiden(lw, EXPAND_MITER, EXPAND_MITER); //using expand_miter line cap, which is no line cap at all, also use miter joins - this is how lines are drawn normally
    } else {
        if (Flip(side, flip ^ forward)==ESingleArrowSide::LEFT)
            path2.Invert();
        ret = path2.SimpleWidenAsymmetric(lw, 0);
    }
    if (t & CLIP_ADD_COVER) {
        if (cached_cover)
            ret += *cached_cover;
        else
            ret += Cover(path, pos, forward, flip, mainline_before, mainline_after, front, back);
    } else if ((t & CLIP_MINUS_COVER) && !ret.IsEmpty()) {
        //if we are half only, we better deduct cover as if we were on both side
        //This avoids having a tiny part of the cover to remain after the substraction
        //due to contour library impreciseness.
        if (side!=ESingleArrowSide::BOTH) {
            SingleArrowHead ah(*this);
            ah.side = ESingleArrowSide::BOTH;
            ret -= ah.Cover(path, pos, forward, flip, mainline_before, mainline_after, front, back);
        } else if (cached_cover)
            ret -= *cached_cover;
        else
            ret -= Cover(path, pos, forward, flip, mainline_before, mainline_after, front, back);
    }
    return ret;
}

/** Draws the arrowhead on a potentially curvy and bendy path.
 * @param canvas The canvas to draw onto.
 *               If the canvas is not doing graphics, but shape output (e.g., PPT),
 *               we emit the necessary shapes - even though for such output we prefer
 *               using the arrowhead attribute of lines to actually emitting shapes
 *               for the arrowhead.
 * @param [in] path The path onto which the arrowhead is drawn.
 * @param [in] pos The position where the tip of the arrowhead shall be on the path.
 * @param [in] forward If true the arrowhead points towards the end of the path (and is thus fully *before* 'pos').
 * @param [in] flip If true, the arrowhead needs to be flipped to the arrow line.
 *                  useful for the 'half' style, which is not symmetric to the arrow line.
 * @param [in] mainline_before The line style of the path before the tip (as seen from the arrowhead's perspective).
 *                             If 'forward' is false, this is actually the line style from tip towards the end of the path.
 * @param [in] mainline_after The line style of the path after the tip (as seen from the arrowhead's perspective).
 * @param [in] front A pointer to the first arrow in the series (may be ==this, if we are first)
 *                   (We assume elements of the arrow series come one after the other continuously in
 *                   memory, so that 'this-1' is the element before us (if this!=front && front!=nullptr)
 *                   It may be nullptr, if we are not part of an arrow series, but stand alone.
 * @param [in] back The iterator pointing to the last arrow in our series. It may be ==this, if
 *                  we are the last arrow. It may also ==front, if we are alone in the series.
 *                  It may be nullptr, if we are not part of an arrow series, but stand alone.
 * @param [in] cached_cover A pointer pointing to a contour holding the result of a previous call to Cover()
 *                          with the *exact same parameters* as to this call; or nullptr if no such contour is
 *                          available. (Then we call it ourselves.) This is to speed up processing.*/
void SingleArrowHead::Draw(Canvas & canvas, const Path & path, const PathPos & pos, bool forward, bool flip,
                           const LineAttr & mainline_before, const LineAttr & mainline_after,
                           const SingleArrowHead *front, const SingleArrowHead *back,
                           const Contour *cached_cover) const
{
    _ASSERT(GoodForArrows(type));
    _ASSERT(line.IsComplete());
    if (type == ESingleArrowType::NONE) return;

    Contour local_cover;
    const Contour &cover = cached_cover ? *cached_cover :
        local_cover = Cover(path, pos, forward, flip, mainline_before, mainline_after, front, back);

    if (type == ESingleArrowType::JUMPOVER) {
        if (mainline_before==mainline_after) {
            Contour c = cover.CreateExpand(-mainline_before.LineWidth()/2, EXPAND_MITER);
            Path p = c.CreatePathFromVisible();
            Path path2 = c.CreatePathFromSelected([](const Edge&e) {return !e.IsVisible(); });
            if (path2.size() && p.size()) {
                p.prepend(path2.GetTangent(path2.GetEndPos(), true, false));
                p.append(path2.GetTangent(path2.GetStartPos(), false, false));
            }
            //Here we don't simply use canvas.Line for double and triple lines,
            //as that wants to untangle and causes some visual errors. Do it again here by hand.
            if (mainline_before.IsDoubleOrTriple()) {
                Path pp1 = p.CreateSimpleExpand(mainline_before.Spacing(), false);
                Path pp2 = p.CreateSimpleExpand(-mainline_before.Spacing(), false);
                LineAttr line = mainline_before;
                line.type = ELineType::SOLID;
                if (canvas.does_graphics()) {
                    canvas.Clip(cover);
                    canvas.Line(pp1, line);
                    canvas.Line(pp2, line);
                    canvas.UnClip();
                } else {
                    pp1.ClipRemove(cover);
                    pp2.ClipRemove(cover);
                    canvas.Add(GSPath(std::move(pp1), line));
                    canvas.Add(GSPath(std::move(pp2), line));
                }
                if (mainline_before.IsTriple()) {
                    line.width = mainline_before.TripleMiddleWidth();
                    if (canvas.does_graphics())
                        canvas.Line(p, line);
                    else
                        canvas.Add(GSPath(std::move(p), line));
                }
            } else {
                if (canvas.does_graphics()) {
                    canvas.Clip(cover);
                    canvas.Line(p, mainline_before);
                    canvas.UnClip();
                } else {
                    p.ClipRemove(cover);
                    canvas.Add(GSPath(std::move(p), mainline_before));
                }
            }
        } else {
            //find the marked edge.  That is where we switch from the smaller to the large linewidth
            if (cover.IsEmpty()) return;
            _ASSERT(cover.CreatePathFromSelected([](const Edge &e) {return e.IsInternalMarked(); }).size()==1);
            auto marked = std::ranges::find_if(cover[0].Outline().GetEdges(),
                                               [](const Edge &e) {return e.IsInternalMarked(); });
            _ASSERT(marked != cover[0].Outline().GetEdges().end());
            if (marked == cover[0].Outline().GetEdges().end()) return;
            XY xy = path.GetPoint(pos);
            XY dir = marked->GetStart() - xy;
            if (dir.length()<0.0001) return;//full circle or very small, we are done.
            const auto who = WidthHeightOffset(mainline_before, mainline_after, front, back);
            dir.Normalize() *= std::max(who.before_x_extent, who.after_x_extent)*2;

            Contour circle(xy, who.before_x_extent-mainline_before.LineWidth()/2);
            circle *= path.SimpleWidenAsymmetric(who.half_height*2, 0).SetVisible(false);
            Path path2(circle.CreatePathFromSelected([](const Edge&e) {return !e.IsVisible(); }));
            Path p = circle.CreatePathFromVisible();
            if (forward) {
                p.prepend(path2.GetTangent(path2.GetEndPos(), true, false));
                p.append(path2.GetTangent(path2.GetStartPos(), false, false));
            } else {
                p.prepend(path2.GetTangent(path2.GetStartPos(), false, false));
                p.append(path2.GetTangent(path2.GetEndPos(), true, false));
            }

            const XY block1[4] = {xy-dir, xy-dir + dir.Rotate90CCW(), xy+dir + dir.Rotate90CCW(), xy+dir};
            const Contour clip1 = cover * Contour(block1);
            const XY block2[4] = { xy-dir, xy-dir + dir.Rotate90CW(), xy+dir + dir.Rotate90CW(), xy+dir };
            const Contour clip2 = cover * Contour(block2);
            if (canvas.does_graphics()) {
                canvas.Clip(Contour(block1)*cover);
                canvas.Line(p, mainline_before);
                canvas.UnClip();
                canvas.Clip(Contour(block2)*cover);
                canvas.Line(p, mainline_after);
                canvas.UnClip();
            } else {
                Path p2 = p;
                p.ClipRemove(clip1);
                p2.ClipRemove(clip2);
                canvas.Add(GSPath(std::move(p), mainline_before));
                canvas.Add(GSPath(std::move(p2), mainline_after));
            }
        }
        return;
    }

    if (type==ESingleArrowType::TICK) {
        const auto who = WidthHeightOffset(mainline_before, mainline_after, front, back);
        const XY tip = path.GetPoint(pos);
        const XY delta_parallel = (path.NextTangentPoint(pos, false)-tip).Normalize();
        const XY delta_perpendicular = delta_parallel.Rotate90CCW()*who.half_height;
        std::array<XY, 2> p;
        switch (side) {
        default:
            _ASSERT(0); FALLTHROUGH;
        case ESingleArrowSide::BOTH:
            p = { tip-delta_perpendicular, tip+delta_perpendicular }; break;
        case ESingleArrowSide::LEFT:
            p = { tip, tip+delta_perpendicular }; break;
        case ESingleArrowSide::RIGHT:
            p = { tip-delta_perpendicular, tip }; break;
        }
        if (canvas.does_graphics())
            canvas.Line(p[0], p[1], line);
        else
            canvas.Add(GSPath(Path(p), line));
    }

    if ((type==ESingleArrowType::LINE || type==ESingleArrowType::CURVE)
            && side != ESingleArrowSide::BOTH) {
        Path p = cover.CreatePathFromVisible();
        p.SimpleExpand(-line.LineWidth()/2);
        if (mainline_before.IsDoubleOrTriple()) {
            //if we come after another half, clip more for non-single arrow lines
            auto who = WidthHeightOffset(mainline_before, mainline_after, front, back);
            const Contour clip = cover - Path(path.CopyPart(path.MovePos(who.before_x_extent, pos), pos)).
                SimpleWiden(mainline_before.LineWidth() - *mainline_before.width, EXPAND_MITER, EXPAND_MITER);
            if (canvas.does_graphics()) canvas.Clip(clip);  //UnClip is in the above block
            else p.ClipRemove(clip);
        } else
            if (canvas.does_graphics()) canvas.Clip(cover); //UnClip is in the above block
            else p.ClipRemove(cover);

        if (canvas.does_graphics()) {
            const cairo_line_cap_t old = canvas.SetLineCap(CAIRO_LINE_CAP_ROUND);
            canvas.Line(p, line);
            canvas.UnClip(); //Clip is in the above block
            canvas.SetLineCap(old);
        } else
            canvas.Add(GSPath(std::move(p), line));
        return;
    }

    if (IsLine(type) || empty) {
        Contour c = cover.CreateExpand(-line.LineWidth()/2);
        if (canvas.does_graphics()) {
            const cairo_line_cap_t old = canvas.SetLineCap(CAIRO_LINE_CAP_ROUND);
            canvas.Line(c, line); //Shrink by linewidth so that the above contours are outer edge
            canvas.SetLineCap(old);
        } else
            canvas.Add(GSPath(c, line));
    } else
        if (canvas.does_graphics())
            canvas.Fill(cover, FillAttr::Solid(*line.color));
        else
            canvas.Add(GSShape(cover, LineAttr::None(), FillAttr::Solid(*line.color)));
}


/** Return the detailed dimensions of a block arrowhead.
 * @param [in] body_height The height (if the arrow is horizontal) of the arrow.
 *                         The arrowhead may extend beyond this body.
 * @param [in] line The line we will use to draw the arrow. Used for linewidth.
 * @param [in] middle Should be true if this arrowhead is not at the end of the
 *                    arrow, but somewhere in the middle of it. DIAMOND_EMPTY
 *                    and DOT_EMPTY draws differently in the middle.
 * @returns the structure containing all the dimensions.*/
SingleArrowHead::BlockWHRetval
SingleArrowHead::BlockWidthHeight(double body_height [[maybe_unused]],
                                  const LineAttr & line, bool middle) const
{
    _ASSERT(GoodForBlockArrows(type));
    _ASSERT(body_height>1);
    const double lw = line.LineWidth();
    BlockWHRetval ret;
    switch (type) {
    case ESingleArrowType::NONE:
    default:
        ret = {0,0,0,0,0,-1,0,0};
        break;
    case ESingleArrowType::NORMAL: /* Normal triangle */
        ret.y_extent_above_body = baseBlockSize*scale.y + lw;
        ret.before_x_extent = 2*baseBlockSize*scale.x + lw*2;
        ret.after_x_extent = 0;
        ret.before_margin = ret.before_x_extent + lw;
        ret.after_margin = 0;
        ret.add_activation = -1;
        ret.body_connect_before = ret.before_x_extent;
        ret.body_connect_after = 0;
        break;
    case ESingleArrowType::SHARP: /* Sharp triangle */
        ret.y_extent_above_body = baseBlockSize*scale.y + lw;
        ret.before_x_extent = SHARP_MUL_1*2*baseBlockSize*scale.x + lw*2;
        ret.after_x_extent = 0;
        ret.before_margin = ret.before_x_extent + lw*2;
        ret.after_margin = 0;
        ret.add_activation = -1;
        ret.body_connect_before = SHARP_MUL_2*2*baseBlockSize*scale.x + lw;
        ret.body_connect_after = 0;
        break;
    case ESingleArrowType::VEE: /* Small triangle */
    case ESingleArrowType::INV: /* Inverse small triangle */
        ret.y_extent_above_body = 0;
        ret.before_x_extent = 2*baseBlockSize*scale.x;
        ret.after_x_extent = 0;
        ret.before_margin = ret.before_x_extent + lw;
        ret.after_margin = 0;
        ret.add_activation = -1;
        ret.body_connect_before = ret.before_x_extent;
        ret.body_connect_after = 0;
        break;
    case ESingleArrowType::NSDIAMOND:
    case ESingleArrowType::NSDOT:
    case ESingleArrowType::NSBOX:
        ret.y_extent_above_body = 0;
        ret.before_x_extent = middle ? 2*baseBlockSize*scale.x + lw/2 : 0;
        ret.after_x_extent = middle ? 2*baseBlockSize*scale.x + lw/2 : 2*baseBlockSize*scale.x + lw;
        ret.before_margin = ret.before_x_extent + (middle ? lw/2 : 0);
        ret.after_margin = ret.after_x_extent + lw/2;
        ret.add_activation = middle ? 0: +1;
        ret.body_connect_before = 0;
        ret.body_connect_after = 0;
        break;
    case ESingleArrowType::DOT:
    case ESingleArrowType::DIAMOND:
    case ESingleArrowType::BOX:
        ret.y_extent_above_body = baseBlockSize*scale.y + lw;
        ret.before_x_extent = ret.after_x_extent = 2*baseBlockSize*scale.x + lw*2;
        ret.before_margin = ret.after_margin = ret.before_x_extent + lw;
        ret.add_activation = 0;
        ret.body_connect_before = 0;
        ret.body_connect_after = 0;
        break;
    case ESingleArrowType::TEE:
        ret.y_extent_above_body = baseBlockSize*scale.y + lw;
        ret.before_x_extent = baseBlockSize/3*scale.x + lw*2;
        ret.after_x_extent = 0;
        ret.before_margin = ret.before_x_extent + lw;
        ret.after_margin = 0;
        ret.add_activation = -1;
        ret.body_connect_before = 0;
        ret.body_connect_after = 0;
        break;
    case ESingleArrowType::STRIPES:
        ret.y_extent_above_body = 0;
        ret.before_x_extent = 4*baseBlockSize*scale.x/6 + 4*lw + 5**line.width + 2;
        ret.after_x_extent = 0; //lw?
        ret.before_margin = ret.before_x_extent + lw*2;
        ret.after_margin = 0;
        ret.add_activation = -1;
        ret.body_connect_before = ret.before_x_extent;
        ret.body_connect_after = 0;
        break;
    case ESingleArrowType::TRIANGLE_STRIPES:
        ret.y_extent_above_body = 0;
        ret.before_x_extent = 6*baseBlockSize*scale.x/6 + 4*lw + 5**line.width + 2;
        ret.after_x_extent = 0; //lw?
        ret.before_margin = ret.before_x_extent + lw*2;
        ret.after_margin = 0;
        ret.add_activation = -1;
        ret.body_connect_before = ret.before_x_extent;
        ret.body_connect_after = 0;
        break;
    }
    return ret;
}

/** Determines the margin required for text inside a block arrow.
 * @param [in] text_cover The area covered by the text. Its position is
 *                        interpreted only in relation to sy and dy.
 *                        The text should be oriented horizontally.
 * @param [in] left_margin If we want the margin at the left side (or the right)
 * @param [in] sy The (outer) top of the body of the arrow. Interpreted in relation to text_cover.
 *                Should be above the top of text_cover's bounding box.
 * @param [in] dy The (outer) bottom of the body of the arrow. Interpreted in relation to text_cover.
 *                Should be below the top of text_cover's bounding box.
 * @param [in] line The line type used to draw the arrowhead. Used for linewidth.
 * @returns How many pixels are required between the tip of the arrowhead to the left/right
 *          side of the text_cover. Note that the term 'tip of the arrowhead' may be inside
 *          the arrowhead itself, e.g., for DOT and represents the point on the target line
 *          the arrow points to. Thus, the value returned may be negative, in which case the
 *          text itself may extend into the target (inside the arrowhead).*/
double SingleArrowHead::BlockTextMargin(Contour text_cover, bool left_margin,
                                        double sy, double dy,
                                        const LineAttr & line) const
{
    _ASSERT(GoodForBlockArrows(type));
    const double lw = line.LineWidth();
    if (type == ESingleArrowType::NONE) return lw;
    auto who = BlockWidthHeight(dy-sy, line, false);
    Contour arrow_head = BlockContour(0, sy, dy, line, false);
    if (left_margin)
        arrow_head.Transform(TRMatrix::CreateFlipYAxis(0));
    Block b = arrow_head.GetBoundingBox().CreateExpand(1);
    const double max = std::max(who.body_connect_before, std::max(fabs(b.x.from), fabs(b.x.till)))+1+100*lw;
    if (left_margin) {
        arrow_head += Block(who.body_connect_before, max, sy, dy); //add arrow block
        b.x.till = max-lw-1;
    } else {
        arrow_head += Block(-max, -who.body_connect_before, sy, dy);
        b.x.from = -max+lw+1;
    }
    arrow_head.Expand(-lw);
    arrow_head = Contour(b) - arrow_head;
    const Range left_right = text_cover.GetBoundingBox().x;
    text_cover.Rotate(90);
    arrow_head.Rotate(90);
    double off, tp;
    if (left_margin) {
        off = arrow_head.OffsetBelow(text_cover, tp, CONTOUR_INFINITY);
        //now tp is negative and shows the margin
        return left_right.from - off;
    } else {
        off = text_cover.OffsetBelow(arrow_head, tp, CONTOUR_INFINITY);
        //now off is negative and smaller than tp, which is probably negative, too
        return -left_right.till-off;
    }
}

/** Returns the outer edge of a block arrowhead.
 * @param [in] x The tip of the arrowhead (at the outer edge)
 * @param [in] sy The top (outer) edge of the arrow body.
 * @param [in] dy The bottom (outer) edge of the arrow body.
 * @param [in] line The line style used to draw the arrow.
 * @param [in] middle True if this arrowhead is at the middle of an arrow,
 *                    false if it is an ultimate end or start.*/
Contour SingleArrowHead::BlockContour(double x, double sy, double dy,
                                      const LineAttr & line, bool middle) const
{
    _ASSERT(GoodForBlockArrows(type));
    const double lw = line.LineWidth();
    auto who = BlockWidthHeight(dy-sy, line, middle);
    const double mid_y = (sy+dy)/2;
    Contour area;
    switch (type) {
    default:
    case ESingleArrowType::NONE:
        break;

    //all these below draw a triangle
    case ESingleArrowType::NORMAL:
    case ESingleArrowType::VEE:
        area = Contour(x, mid_y,
                       x-who.before_x_extent, sy-who.y_extent_above_body,
                       x-who.before_x_extent, dy+who.y_extent_above_body);
        break;

    case ESingleArrowType::SHARP:
    {
        const XY p[4] = {{x, mid_y},
                         {x-who.before_x_extent, dy+who.y_extent_above_body},
                         {x-who.before_x_extent*SHARP_MUL_2, mid_y},
                         {x-who.before_x_extent, sy-who.y_extent_above_body}};
        area.assign_dont_check(p);
        break;
    }

    case ESingleArrowType::INV:
        switch (side) {
        default: _ASSERT(0);
            FALLTHROUGH;
        case ESingleArrowSide::BOTH:
            area = Contour(x-who.before_x_extent, mid_y, x-who.before_x_extent, sy, x, sy);
            area += Contour(x-who.before_x_extent, mid_y, x-who.before_x_extent, dy, x, dy);
            break;
        case ESingleArrowSide::LEFT:
            area = Contour(x, sy, x-who.before_x_extent, dy, x-who.before_x_extent, sy);
            break;
        case ESingleArrowSide::RIGHT:
            area = Contour(x, dy, x-who.before_x_extent, dy, x-who.before_x_extent, sy);
            break;
        }
        return area; //we are done doing the sides.

    case ESingleArrowType::NSDIAMOND: /* who.y_extent_above_body is zero here */
        if (middle) break;
        FALLTHROUGH;
    case ESingleArrowType::DIAMOND:
        area = diamond(XY(x, mid_y),
                       XY(who.after_x_extent, fabs(dy-sy)/2+who.y_extent_above_body));
        break;

    case ESingleArrowType::NSDOT: /* who.y_extent_above_body is zero here */
        if (middle) break;
        FALLTHROUGH;
    case ESingleArrowType::DOT:
        area += Contour(XY(x, mid_y),
                        who.after_x_extent,
                        fabs(dy-sy)/2 + who.y_extent_above_body);
        break;

    case ESingleArrowType::NSBOX: /* who.y_extent_above_body is zero here */
        if (middle) break;
        FALLTHROUGH;
    case ESingleArrowType::TEE:
    case ESingleArrowType::BOX:
        area += Contour(x-who.before_x_extent, x+who.after_x_extent,
                        sy - who.y_extent_above_body, dy + who.y_extent_above_body);
        break;

    case ESingleArrowType::STRIPES:
    {
        const double a = baseBlockSize*scale.x/6;
        //first box is LW+(a+line.width)+LW big
        //second box is LW+(3*a+2*line.width)+LW big
        //with a line.width+1 space in between and after the second box
        //who.before_x_extent equals to the sum of the above, which is 4*a+4*LW+5*l.width.
        //"a" depends on arrowhead size
        double width = 2*lw + a + *line.width;
        area = Block(x - width, x, sy, dy);
        x -= width + *line.width + 1;
        width += 2*a + *line.width;
        area += Block(x - width, x, sy, dy);
        break;
    }
    case ESingleArrowType::TRIANGLE_STRIPES:
    {
        //same spacing as above, but there is an indentation of size "2*a"
        //also total width is "2*a" wider
        const double a = baseBlockSize*scale.x/6;
        double width = 2*lw + a + *line.width;
        double xx = x;
        for (unsigned u = 0; u<2; u++) {
            const XY p[6] = {{xx, sy},
                             {xx - 2*a, mid_y},
                             {xx, dy},
                             {xx - width, dy},
                             {xx - width - 2*a, mid_y},
                             {xx - width, sy}};
            area.append_dont_check(p);
            xx -= width + *line.width + 1;
            width += 2*a + *line.width;
        }
        //x2 should == xx-2*a, but due to division by 6, we may sometimes get
        //an imprecise result - which will make the small triangles not to be
        //merged with the body.
        const double x2 = x - 4*lw - baseBlockSize*scale.x - 5**line.width - 2;
        //add the 2 small triangles
        area += Contour(XY(xx, sy), XY(x2, mid_y), XY(x2, sy));
        area += Contour(XY(xx, dy), XY(x2, mid_y), XY(x2, dy));
        break;
    }
    }
    return area;
}

/** Return additional lines to draw if the arrowhead is in the middle of an arrow.
 * (Primarily for the circle or diamond for DOT_EMPTY or DIAMOND_EMPTY.) */
Path SingleArrowHead::BlockMidPath(double x, double sy, double dy, const LineAttr & line) const
{
    _ASSERT(GoodForBlockArrows(type));
    if (type!=ESingleArrowType::NSDOT && type!=ESingleArrowType::NSDIAMOND && type!=ESingleArrowType::NSBOX)
        return Path();
    auto who = BlockWidthHeight(dy-sy, line, true);
    const XY center(x, (dy+sy)/2);
    const double lw2 = line.LineWidth()/2;
    const XY wh(who.before_x_extent - lw2, (dy-sy)/2 - lw2);
    switch (type) {
    default: _ASSERT(0); FALLTHROUGH;
    case ESingleArrowType::NSDOT:
        return Contour(center, wh.x, wh.y);
    case ESingleArrowType::NSDIAMOND:
        return diamond(center, wh);
    case ESingleArrowType::NSBOX:
        return Contour(Block(center-wh, center+wh));
    }
    _ASSERT(0);
    return Path();
}

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

const ArrowHead ArrowHead::noneArrowHead(EArrowType::NONE, false, EArrowSize::NORMAL);

/** This is called when the Arrowhead is initialized with a potentially non-single EArrowType.*/
void ArrowHead::Convert(EArrowType t, bool block, const XY & sc, const LineAttr & l)
{
    const unsigned dup = Duplicity(t);
    single = SingleArrowHead(t, block, sc, l);
    if (!block && dup>1 )
        series.assign(dup, single);
    TestIfSingle();
}

ArrowHead::ArrowHead(std::initializer_list<EArrowType> l, bool block, EArrowSize s, const LineAttr &line)
{
    _ASSERT(s!=EArrowSize::INVALID);
    const XY scale(ArrowScale(s), ArrowScale(s));
    for (auto t : l)
        for (int i = block ? 1 : Duplicity(t); i>0; i--)
            series.emplace_back(t, block, scale, line);
    TestIfSingle();
}

/** Append an arrowhead to a (potentially empty) series.
 * Returns false on error (which can only be if
 * any of scale.x or .y is non-positive or l is not complete.*/
bool ArrowHead::Append(ESingleArrowType t, ESingleArrowSide s, bool o, const XY & sc, const LineAttr & l)
{
    if (t==ESingleArrowType::NONE)
        return true;
    if (sc.x <= 0 || sc.y<=0)
        return false;
    if (!l.IsComplete())
        return false;
    PrepToAdd();
    if (series.size())
        series.emplace_back(t, s, o, sc, l);
    else
        single = {t, s, o, sc, l};
    return true;
}

/** Append an arrowhead to a (potentially empty) series.
 * Returns false on error (which can only be if t==INVALID
 * or any of scale.x or .y is non-positive or l is not complete.*/
bool ArrowHead::Append(EArrowType t, bool block, const XY &scale, const LineAttr &l)
{
    if (t==EArrowType::NONE)
        return true;
    if (t==EArrowType::INVALID)
        return false;
    if (scale.x <= 0 || scale.y<=0)
        return false;
    if (!l.IsComplete())
        return false;
    PrepToAdd();
    for (unsigned count = block ? 1 : Duplicity(t); count > 0; count--)
        series.emplace_back(t, block, scale, l);
    TestIfSingle();
    return true;
}

bool ArrowHead::Prepend(ESingleArrowType t, ESingleArrowSide s, bool o, const XY & sc, const LineAttr & l)
{
    if (t==ESingleArrowType::NONE)
        return true;
    if (sc.x <= 0 || sc.y<=0)
        return false;
    if (!l.IsComplete())
        return false;
    PrepToAdd();
    if (series.size())
        series.emplace(series.begin(), t, s, o, sc, l);
    else
        single = {t, s, o, sc, l};
    return true;
}

/** Append an arrowhead to a (potentially empty) series.
 * Returns false on error (which can only be if t==INVALID
 * or any of scale.x or .y is non-positive or l is not complete.*/
bool ArrowHead::Prepend(EArrowType t, bool block, const XY & scale, const LineAttr & l)
{
    if (t==EArrowType::NONE)
        return true;
    if (t==EArrowType::INVALID)
        return false;
    if (scale.x <= 0 || scale.y<=0)
        return false;
    if (!l.IsComplete())
        return false;
    PrepToAdd();
    for (unsigned count = block ? 1 : Duplicity(t); count > 0; count--)
        series.emplace(series.begin(), t, block, scale, l);
    TestIfSingle();
    return true;
}

/** Parses an (extended) graphviz arrowhead attribute string.
 * It can be 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).
 * What we added is a few more 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"
 * With us arrowheads may be separated by '|' pipe symbols.
 * E.g., "tinyreddot|small|black|normal"
 * Use 'sizenormal' for 'normal' size which collides with 'normal' type.
 *
 * In addition you can add attributes (always terminated by a pipe):
 * lwidth=x, 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".
 *
 * The function can output either an ArrowHead and/or a sanitized string
 * that can be fed to this function again (and guarantees not to throw an error)
 * (and guarantees not to contain any color tokens, only rgb values).
 * If you call it with chart==nullptr, no errors will be generated, but we will not
 * parse line attributes (will jump them over). This is only useful to see if
 * a previously sanitized string is good for a particular arrow type (block/normal)
 *
 * @param [in] attr The text to parse
 * @param [in] at What kind of arrow do we parse for block/arrow/any (shall not be NONE)
 * @param [in] line The line style to start with (includes color)
 * @param [in] scale The scale to start with
 * @param [in] colors What color names do we recognize, may be null if we dont want this.
 * @param [in] file_pos The starting position of 'attr' in the input file.
 * @param chart The chart to generate errors to. We do not have a version of
 *              this function that does not generate errors.
 * @param [out] arrow If non-nullptr the result of parsing is here.
 * @param [out] replaceto If non-nullptr the sanitized string is placed here.
 * @returns true on error, when we have found a totally unrecognizable
 *  arrowhead type or attribute. On smaller errors, e.g., when having multiple
 *  segments for block arrows, bad values for xmul, etc. we return false, but
 *  generate errors. If we return false, replaceto and arrow will be empty.*/
bool ArrowHead::ParseGraphviz(std::string_view attr, EArcArrowType at, LineAttr line, XY scale,
                              const ColorSet *colors, FileLineCol file_pos, Chart *chart,
                              ArrowHead *arrow, std::string *replaceto)
{
    _ASSERT(at!=EArcArrowType::NONE);
    if (at==EArcArrowType::NONE)
        at = EArcArrowType::ANY;
    static const char ah_names[][17] = {
        "none", "normal", "sdiamond", "diamond", "sdot", "dot", "sbox", "box",
        "sharp", "inv", "vee", "tee", "line", "crow", "curve", "icurve", "tick", "jumpover",
        "stripes", "triangle_stripes", ""
    };
    struct hit
    {
        std::string token;
        enum EKind { TYPE, SIZE, COLOR, ATTR } kind;
        unsigned index;
        ESingleArrowSide side;
        bool empty;
        ColorType color;
        bool valid;
        hit(std::string && t, EKind k, unsigned i, ESingleArrowSide s, bool e, ColorType c, bool v) :
            token(std::move(t)), kind(k), index(i), side(s), empty(e), color(c), valid(v) {}
        bool operator < (const hit&o) const { return token<o.token; }
    };
    thread_local std::set<hit> fix_tokens;
    if (fix_tokens.size()==0) {
        //Add arrowhead names
        for (unsigned u = 0; ah_names[u][0]; u++)
            for (unsigned o = 0; o<2; o++) {
                const std::string oprefix(o ? "o" : "");
                fix_tokens.emplace(oprefix + ah_names[u], hit::TYPE, u,
                    ESingleArrowSide::BOTH, o==1, ColorType(), o==0 || CanBeEmpty(ESingleArrowType(u)));
                fix_tokens.emplace(oprefix + "l" + ah_names[u], hit::TYPE, u,
                    ESingleArrowSide::LEFT, o==1, ColorType(), (o==0 || CanBeEmpty(ESingleArrowType(u))) && CanBeHalf(ESingleArrowType(u)));
                fix_tokens.emplace(oprefix + "r" + ah_names[u], hit::TYPE, u,
                    ESingleArrowSide::RIGHT, o==1, ColorType(), (o==0 || CanBeEmpty(ESingleArrowType(u))) && CanBeHalf(ESingleArrowType(u)));
            }
        //Add arrowhead sizes
        for (unsigned u = 1; EnumEncapsulator<EArrowSize>::names[u][0]; u++)
            fix_tokens.emplace(EnumEncapsulator<EArrowSize>::names[u], hit::SIZE, u,
                ESingleArrowSide::BOTH, false, ColorType(), true);
        //add this to resolve ambiguity with 'normal' arrow type
        fix_tokens.emplace("sizenormal", hit::SIZE, unsigned(EArrowSize::NORMAL),
            ESingleArrowSide::BOTH, false, ColorType(), true);
        //add attributes
        fix_tokens.emplace("color=", hit::ATTR, 0,
            ESingleArrowSide::BOTH, false, ColorType(), true);
        fix_tokens.emplace("ltype=", hit::ATTR, 1,
            ESingleArrowSide::BOTH, false, ColorType(), true);
        fix_tokens.emplace("lwidth=", hit::ATTR, 2,
            ESingleArrowSide::BOTH, false, ColorType(), true);
        fix_tokens.emplace("xmul=", hit::ATTR, 3,
            ESingleArrowSide::BOTH, false, ColorType(), true);
        fix_tokens.emplace("ymul=", hit::ATTR, 4,
            ESingleArrowSide::BOTH, false, ColorType(), true);
        fix_tokens.emplace("mul=", hit::ATTR, 5,
            ESingleArrowSide::BOTH, false, ColorType(), true);

    }
    //if no colors were received, we use the stock 'fix_tokens' set to speed up
    std::set<hit> tokens_with_color;
    if (colors && colors->size()) {
        //copy is expensive, but happens only when user types an actual string in the
        //input file - on any later parse we will have had color names replaced.
        tokens_with_color = fix_tokens;
        //Insert colors last, so if they coincide with an arrow name or size,
        //they get silently overwritten and the latter takes precedence.
        for (auto &c : *colors)
            tokens_with_color.emplace(std::string(c.first), hit::COLOR, 0,
                ESingleArrowSide::BOTH, false, c.second, true);
    }
    const std::set<hit> &tokens = colors && colors->size() ? tokens_with_color : fix_tokens;
    unsigned count_arrowheads = 0;
    while (attr.length()) {
        //jump over pipes and spaces
        if (attr.front()=='|' || attr.front()==' ' || attr.front()=='\t') {
            attr.remove_prefix(1);
            file_pos.col++;
            continue;
        }
        //find the shortest prefix of 'attr' that is equal to one element in 'tokens'
        const hit* full_match = nullptr;
        for (unsigned length = 1; full_match==nullptr; length++) {
            unsigned no_match = 0;
            for (const auto &t : tokens)
                if (strncmp(attr.data(), t.token.c_str(), length)==0) {
                    if (length == t.token.length())
                        full_match = &t;
                    no_match++;
                }
            if (no_match==0) break;
        }
        if (full_match) {
            //OK, we have found a valid token
            switch (full_match->kind) {
            default: _ASSERT(0); break;
            case hit::TYPE:
                if (full_match->valid) {
                    if (!GoodForThisArrowType(at, ESingleArrowType(full_match->index))) {
                        if (chart)
                            chart->Error.Error(file_pos, "This arrowhead type ('" +
                                                    full_match->token +
                                                    "') is " + (at==EArcArrowType::ARROW ? "applicable only" : "not applicable") +" to block arrows",
                                            "Ignoring the entire arrowhead definition.");
                    fail:
                        if (replaceto)replaceto->clear();
                        if (arrow) arrow->clear();
                        return true;
                    }
                    count_arrowheads++;
                    if (count_arrowheads>1 && at==EArcArrowType::BIGARROW) {
                        if (chart)
                            chart->Error.Error(file_pos, "Only one segment can be specified for block arrows. "
                                "Ignoring the entire arrowhead definition.");
                        goto fail;
                    }
                    if (arrow)
                        arrow->Append(ESingleArrowType(full_match->index), full_match->side, full_match->empty, scale, line);
                    if (replaceto)
                        replaceto->append(full_match->token);
                } else {
                    if (chart)
                        chart->Error.Error(file_pos, "This arrowhead type ('" +
                                                  full_match->token +
                                                  "') is not a valid combination of 'o', 'l'/'r' and an arrowhead name. "
                                                  "Ignoring the entire arrowhead definition.");
                    goto fail;
                }
                break;
            case hit::COLOR:
                line.color = full_match->color;
                if (replaceto)
                    replaceto->append("color=").append(line.color->Print());
                break;
            case hit::SIZE:
                scale = {ArrowScale(EArrowSize(full_match->index)), ArrowScale(EArrowSize(full_match->index))};
                if (replaceto)
                    replaceto->append(full_match->token);
                break;
            case hit::ATTR:
                auto e = attr.begin() + full_match->token.length();
                while (*e==' ' || *e=='\t') e++; //skip over whitespace
                auto p = std::find(e, attr.end(), '|'); //may point to end of string if '|' not found
                FileLineCol linenum_value = file_pos;
                linenum_value.col += UTF8len(attr.substr(0, e-attr.begin()));
                std::string avalue(e, p);
                if (avalue.length()==0) {
                    if (chart)
                        chart->Error.Error(linenum_value, "Missing value. Ignoring this.");
                } else {
                    double value = 0;
                    if (full_match->index>=2) {
                        //we need a number as param for these attrs
                        if ((avalue[0]<'0' || avalue[0]>'9') && avalue[0]!='.') {
                            //must be a number
                            if (chart)
                                chart->Error.Error(linenum_value, "Missing a positive number instead of '"+avalue+"'. Ignoring this.");
                            value = -1;
                        } else {
                            if (from_chars(avalue, value) || value<0.1 || value>10) {
                                if (chart)
                                    chart->Error.Error(linenum_value, "Need a value between [0.1, 10] instead of '"+avalue+"'. Ignoring this.");
                                value = -1;
                            } else
                                avalue = std::to_string(value); //sanitize value string
                        }
                    }
                    //value==-1 signals that we ignore this
                    if (value>=0) switch (full_match->index) {
                    default: _ASSERT(0); break;
                    case 1: /*line.type*/
                        if (ELineType t; ::Convert(avalue, t)) {
                            line.type = t;
                            if (replaceto)
                                replaceto->append("ltype=").append(EnumEncapsulator<ELineType>::names[unsigned(*line.type)]);
                        } else {
                            if (chart)
                                chart->Error.Error(linenum_value, "Unrecognized line type: '"+avalue+"'. Ignoring this.",
                                    "Use one of "+CandidatesFor(t)+".");
                        }
                        break;
                    case 2: //line.width
                        line.width = value;
                    good_number:
                        if (replaceto)
                            replaceto->append(full_match->token).append(avalue);
                        break;
                    case 3: //xmul
                        scale.x *= value;
                        goto good_number;
                    case 4: //ymul
                        scale.y *= value;
                        goto good_number;
                    case 5: //mul
                        scale *= value;
                        goto good_number;
                    case 0: /*color*/
                        ColorType color;
                        if (colors)
                            color = colors->GetColor(avalue);
                        else
                            color = ColorType(avalue);
                        if (color.type != ColorType::INVALID) {
                            if (replaceto)
                                replaceto->append("color=").append(color.Print());
                            line.color = color;
                        } else {
                            if (chart && !chart->GetCurrentContext()->SkipContent())
                                chart->Error.Error(linenum_value, "Unrecognized color name or definition: '"+avalue+"'. Ignoring this.");
                        }
                        break;
                    }
                }
                file_pos.col += UTF8len(attr.substr(0, p-attr.begin()));
                attr.remove_prefix(p-attr.begin());
                break;
            }
            if (full_match->kind!=hit::ATTR) {
                //we have already stepped for ATTR
                file_pos.col += UTF8len(full_match->token);
                attr.remove_prefix(full_match->token.length());
            }
        } else if (chart) {
            chart->Error.Error(file_pos, "Unrecognized arrowhead name, size, color or attribute: '"+std::string(attr)+"'.",
                "Ignoring the remainder of this arrowhead definition.");
            goto fail;
        }
        if (replaceto)
            replaceto->append("|");
    }
    return false;
}

/** Tells how tall the arrowhead (series) is and how much it extends
 * left and right of the target line.
 * @param [in] mainline_before The line style of the arrow line from the direction
 *                             we point from (=before the target line). (Used by JUMPOVER type.)
 * @param [in] mainline_after The line style of the arrow line from the direction
 *                            we point to. (=after the target line) (Used by JUMPOVER type.)
 * @returns Half the height (that is, .y equals how much the arrowhead spans above/below
 *          the arrow line), the x size on the arriving side of the arrow
 *          and the extent at the other side of the target.*/
ArrowHead::WHRetval ArrowHead::WidthHeight(const LineAttr &mainline_before, const LineAttr &mainline_after) const
{
    if (IsNone()) return{0,0,0};
    auto ret = front().WidthHeightOffset(mainline_before, mainline_after, fsah(), lsah());
    for (unsigned u = 1; u<series.size(); u++) {
        //use mainline_before twice, for subsequent segments
        auto who = series[u].WidthHeightOffset(mainline_before, mainline_before, fsah(), lsah());
        ret.before_x_extent = std::max(ret.before_x_extent, ret.offset + who.before_x_extent + who.after_x_extent);
        ret.half_height = std::max(ret.half_height, who.half_height);
        ret.offset += who.offset + who.after_x_extent;
    }
    return static_cast<ArrowHead::WHRetval&>(ret);
}

/** Return the area for which the target (e.g., entity line) shall not be drawn.
 * Only dots and diamonds cover it, for others an empty Contour is returned.
 * This version of the function assumes the arrowhead is drawn horizontally from left to right.
 * @param [in] xy The tip of the arrowhead.
 * @param [in] flip If true, the arrowhead needs to be flipped to the arrow line.
 *                  useful for the 'half' style, which is not symmetric to the arrow line.
 * @param [in] mainline_before The line style of the arrow line from the direction
 *                             we point from (=before the target line). (Used by JUMPOVER type.)
 * @param [in] mainline_after The line style of the arrow line from the direction
 *                            we point to. (=after the target line) (Used by JUMPOVER type.)
 * @returns The contour of the dot or diamond.*/
Contour ArrowHead::TargetCover(const XY & xy, bool flip, const LineAttr & mainline_before, const LineAttr & mainline_after) const
{
    if (front().type!=ESingleArrowType::BOX &&
        front().type!=ESingleArrowType::DIAMOND &&
        front().type!=ESingleArrowType::DOT)
        return Contour(); //No other arrow type covers the entity line
    return front().Cover(xy, flip, mainline_before, mainline_after, fsah(), lsah());
}

/** Returns an area where the arrow line should not be drawn.
 * The inverse of it will be used to clip, when drawing the arrow line.
 * Note that we have to consider lines that are thicker than the arrowhead.
 * This version of the function assumes the arrowhead is drawn horizontally from left to right.
 * @param [in] xy The tip of the arrowhead
 * @param [in] flip If true, the arrowhead needs to be flipped to the arrow line.
 *                  useful for the 'half' style, which is not symmetric to the arrow line.
 * @param [in] ignore_opaque When set to true, we will not clip the line for filled arrowheads.
 *                           This may lead to better performance if you draw the arrowhead
 *                           *after* the arrow line and the fill color is opaque.
 * @param [in] mainline_before The line style of the arrow line from the direction
 *                             we point from (=before the target line). (Used by JUMPOVER type.)
 * @param [in] mainline_after The line style of the arrow line from the direction
 *                            we point to. (=after the target line) (Used by JUMPOVER type.)
 * @param [in] mirror If true, this is a mirror of a bidirectional arrowhead.
 *                    E.g., when having ->a<-, the left side is the mirror of the
 *                    right side. When we consider the mirror, we should omit any
 *                    symmetrical element at the front (but consider its offset).
 * @returns An area covering the area where the arrow line should NOT be drawn.
 *          If no clipping is needed (all of the line can be shown at this arrowhead),
 *          we return an empty contour.*/
Contour ArrowHead::ClipForLine(const XY &xy, bool flip, bool ignore_opaque,
                               const LineAttr & mainline_before, const LineAttr & mainline_after,
                               bool mirror [[maybe_unused]]) const
{
    Contour area;
    if (series.size()==0) {
        area = single.ClipLine(xy, flip, ignore_opaque, mainline_before, mainline_after, fsah(), lsah());
    } else {
        XY run = xy;
        for (unsigned u = 0; u<series.size(); u++) {
            auto who = series[u].WidthHeightOffset(mainline_before, u==0 ? mainline_after : mainline_before, fsah(), lsah());
            if (u) run.x -= who.after_x_extent;
            area += series[u].ClipLine(run, flip, ignore_opaque, mainline_before, u==0 ? mainline_after : mainline_before, fsah(), lsah());
            run.x -= who.offset;
        }
    }
    return area;
}

/** Returns a contour covering the arrowhead.
 * This version of the function assumes the arrowhead is drawn horizontally from left to right.
 * @param [in] xy The tip of the arrowhead.
 * @param [in] flip If true, the arrowhead needs to be flipped to the arrow line.
 *                  useful for the 'half' style, which is not symmetric to the arrow line.
 * @param [in] mainline_before The line style of the arrow line from the direction
 *                             we point from (=before the target line). (Used by JUMPOVER type.)
 * @param [in] mainline_after The line style of the arrow line from the direction
 *                            we point to. (=after the target line) (Used by JUMPOVER type.)
 * @param [in] mirror If true, this is a mirror of a bidirectional arrowhead.
 *                    E.g., when having ->a<-, the left side is the mirror of the
 *                    right side. When we consider the mirror, we should omit any
 *                    symmetrical element at the front (but consider its offset).
 * @returns the contour of the arrowhead.*/
Contour ArrowHead::Cover(const XY & xy, bool flip,
                         const LineAttr & mainline_before, const LineAttr & mainline_after,
                         bool mirror) const
{
    Contour area;
    if (series.size()==0) {
        if (!mirror || !StartsWithSymmetricAndNotNone()) //skip the first element if symmetric and mirror==true
            area = single.Cover(xy, flip, mainline_before, mainline_after, fsah(), lsah());
    } else {
        XY run = xy;
        for (unsigned u = 0; u<series.size(); u++) {
            auto who = series[u].WidthHeightOffset(mainline_before, u==0 ? mainline_after : mainline_before, fsah(), lsah());
            if (u) run.x -= who.after_x_extent;
            if (!mirror || u || !::IsSymmetricAndNotNone(series[u].type))
                //skip the first element if symmetric and mirror==true
                area += series[u].Cover(run, flip, mainline_before, u==0 ? mainline_after : mainline_before, fsah(), lsah());
            run.x -= who.offset;
        }
    }
    return area;
}

/** Actually draws the arrowhead to the canvas.
 * This version of the function assumes the arrowhead is drawn horizontally from left to right.
 * @param [in] xy The tip of the arrowhead.
 * @param canvas The canvas to draw on.
 * @param [in] flip If true, the arrowhead needs to be flipped to the arrow line.
 *                  useful for the 'half' style, which is not symmetric to the arrow line.
 * @param [in] mainline_before The line style of the arrow line from the direction
 *                             we point from (=before the target line). (Used by JUMPOVER type.)
 * @param [in] mainline_after The line style of the arrow line from the direction
 *                            we point to. (=after the target line) (Used by JUMPOVER type.)
 * @param [in] mirror If true, this is a mirror of a bidirectional arrowhead.
 *                    E.g., when having ->a<-, the left side is the mirror of the
 *                    right side. When we consider the mirror, we should omit any
 *                    symmetrical element at the front (but consider its offset).
 * @returns the contour of the arrowhead.*/
void ArrowHead::Draw(Canvas &canvas, const XY & xy, bool flip,
                     const LineAttr & mainline_before, const LineAttr & mainline_after,
                     bool mirror) const
{
    if (series.size()==0) {
        if (!mirror || !StartsWithSymmetricAndNotNone()) //skip the first element if symmetric and mirror==true
            single.Draw(canvas, xy, flip, mainline_before, mainline_after, fsah(), lsah());
    } else {
        XY run = xy;
        for (unsigned u = 0; u<series.size(); u++) {
            auto who = series[u].WidthHeightOffset(mainline_before, u==0 ? mainline_after : mainline_before, fsah(), lsah());
            if (u) run.x -= who.after_x_extent;
            if (!mirror || u || !::IsSymmetricAndNotNone(series[u].type))
                //skip the first element if symmetric and mirror==true
                series[u].Draw(canvas, run, flip, mainline_before, u==0 ? mainline_after : mainline_before, fsah(), lsah());
            run.x -= who.offset;
        }
    }
}

/** Return the area for which the target (e.g., entity line) shall not be drawn.
 * Only dots and diamonds cover it, for others an empty Contour is returned.
 * This version of the function assumes the arrowhead is drawn to a straight
 * arrow (of arbitrary direction).
 * @param [in] xy The tip of the arrowhead.
 * @param [in] from Specifies the direction from which the arrowhead is pointed to
 *                  the tip. Suitably rotates the arrowhead (or rather the cover returned)
 *                  to cover a line perpendicular to the one coming from the direction of 'from'.
 * @param [in] flip If true, the arrowhead needs to be flipped to the arrow line.
 *                  useful for the 'half' style, which is not symmetric to the arrow line.
 * @param [in] mainline_before The line style of the arrow line from the direction
 *                             we point from (=before the target line). (Used by JUMPOVER type.)
 * @param [in] mainline_after The line style of the arrow line from the direction
 *                            we point to. (=after the target line) (Used by JUMPOVER type.)
 * @returns The contour of the dot or diamond.*/
Contour ArrowHead::TargetCover(const XY & xy, const XY & from, bool flip,
                               const LineAttr & mainline_before, const LineAttr & mainline_after) const
{
    const double dist = from.Distance(xy);
    _ASSERT(dist>0.0001);
    if (dist<=0.0001) return Contour();
    const double cos = (xy.x-from.x)/dist;
    const double sin = (xy.y-from.y)/dist;
    return TargetCover(xy, flip, mainline_before, mainline_after).RotateAround(xy, cos, sin);
}

/** Returns an area where the arrow line should not be drawn.
 * The inverse of it will be used to clip, when drawing the arrow line.
 * Note that we have to consider lines that are thicker than the arrowhead.
 * This version of the function assumes the arrowhead is drawn to a straight
 * arrow (of arbitrary direction).
 * @param [in] xy The tip of the arrowhead
 * @param [in] from Specifies the direction from which the arrowhead is pointed to
 *                  the tip. Suitably rotates the arrowhead (or rather the cover returned)
 *                  to cover a line coming from the direction of 'from'.
 * @param [in] flip If true, the arrowhead needs to be flipped to the arrow line.
 *                  useful for the 'half' style, which is not symmetric to the arrow line.
 * @param [in] ignore_opaque When set to true, we will not clip the line for filled arrowheads.
 *                           This may lead to better performance if you draw the arrowhead
 *                           *after* the arrow line and the fill color is opaque.
 * @param [in] mainline_before The line style of the arrow line from the direction
 *                             we point from (=before the target line). (Used by JUMPOVER type.)
 * @param [in] mainline_after The line style of the arrow line from the direction
 *                            we point to. (=after the target line) (Used by JUMPOVER type.)
 * @param [in] mirror If true, this is a mirror of a bidirectional arrowhead.
 *                    E.g., when having ->a<-, the left side is the mirror of the
 *                    right side. When we consider the mirror, we should omit any
 *                    symmetrical element at the front (but consider its offset).
 * @returns An area covering the area where the arrow line should NOT be drawn.
 *          If no clipping is needed (all of the line can be shown at this arrowhead),
 *          we return an empty contour.*/
Contour ArrowHead::ClipForLine(const XY & xy, const XY & from, bool flip, bool ignore_opaque,
                               const LineAttr & mainline_before, const LineAttr & mainline_after,
                               bool mirror) const
{
    const double dist = from.Distance(xy);
    _ASSERT(dist>0.0001);
    if (dist<=0.0001) return Contour();
    const double cos = (xy.x-from.x)/dist;
    const double sin = (xy.y-from.y)/dist;
    return ClipForLine(xy, flip, ignore_opaque, mainline_before, mainline_after, mirror).RotateAround(xy, cos, sin);
}

/** Returns a contour covering the arrowhead.
 * This version of the function assumes the arrowhead is drawn to a straight
 * arrow (of arbitrary direction).
 * @param [in] xy The tip of the arrowhead.
 * @param [in] from Specifies the direction from which the arrowhead is pointed to
 *                  the tip. Suitably rotates the arrowhead (or rather the cover returned).
 * @param [in] flip If true, the arrowhead needs to be flipped to the arrow line.
 *                  useful for the 'half' style, which is not symmetric to the arrow line.
 * @param [in] mainline_before The line style of the arrow line from the direction
 *                             we point from (=before the target line). (Used by JUMPOVER type.)
 * @param [in] mainline_after The line style of the arrow line from the direction
 *                            we point to. (=after the target line) (Used by JUMPOVER type.)
 * @param [in] mirror If true, this is a mirror of a bidirectional arrowhead.
 *                    E.g., when having ->a<-, the left side is the mirror of the
 *                    right side. When we consider the mirror, we should omit any
 *                    symmetrical element at the front (but consider its offset).
 * @returns the contour of the arrowhead.*/
Contour ArrowHead::Cover(const XY & xy, const XY & from, bool flip,
                         const LineAttr & mainline_before, const LineAttr & mainline_after,
                         bool mirror) const
{
    const double dist = from.Distance(xy);
    _ASSERT(dist>0.0001);
    if (dist<=0.0001) return Contour();
    const double cos = (xy.x-from.x)/dist;
    const double sin = (xy.y-from.y)/dist;
    return Cover(xy, flip, mainline_before, mainline_after, mirror).RotateAround(xy, cos, sin);
}

/** Actually draws the arrowhead to a canvas.
 * This version of the function assumes the arrowhead is drawn to a straight
 * arrow (of arbitrary direction).
 * @param canvas The canvas to draw on.
 * @param [in] xy The tip of the arrowhead.
 * @param [in] from Specifies the direction from which the arrowhead is pointed to
 *                  the tip. Suitably rotates the arrowhead.
 * @param [in] flip If true, the arrowhead needs to be flipped to the arrow line.
 *                  useful for the 'half' style, which is not symmetric to the arrow line.
 * @param [in] mainline_before The line style of the arrow line from the direction
 *                             we point from (=before the target line). (Used by JUMPOVER type.)
 * @param [in] mainline_after The line style of the arrow line from the direction
 *                            we point to. (=after the target line) (Used by JUMPOVER type.)
 * @param [in] mirror If true, this is a mirror of a bidirectional arrowhead.
 *                    E.g., when having ->a<-, the left side is the mirror of the
 *                    right side. When we consider the mirror, we should omit any
 *                    symmetrical element at the front (but consider its offset).*/
void ArrowHead::Draw(Canvas & canvas, const XY & xy, const XY & from, bool flip,
                     const LineAttr & mainline_before, const LineAttr & mainline_after,
                     bool mirror) const
{
    const double dist = from.Distance(xy);
    _ASSERT(dist>0.0001);
    if (dist<=0.0001) return;
    const double cos = (xy.x-from.x)/dist;
    const double sin = (xy.y-from.y)/dist;
    canvas.Transform_Rotate(xy, cos, sin);
    Draw(canvas, xy, flip, mainline_before, mainline_after, mirror);
    canvas.UnTransform();
}

/** Return the area for which the target (e.g., entity line) shall not be drawn.
 * Only dots and diamonds cover it, for others an empty Contour is returned.
 * This version of the function assumes the arrowhead is drawn to an
 * arbitrary position on an arbitrary path.
 * @param [in] path The arrowhead is along this path.
 * @param [in] pos The tip of the arrowhead is at this position.
 * @param [in] forward True if the arrowhead points towards the end of the path.
 * @param [in] flip If true, the arrowhead needs to be flipped to the arrow line.
 *                  useful for the 'half' style, which is not symmetric to the arrow line.
 * @param [in] mainline_before The line style of the arrow line from the direction
 *                             we point from (=before the target line). (Used by JUMPOVER type.)
 * @param [in] mainline_after The line style of the arrow line from the direction
 *                            we point to. (=after the target line) (Used by JUMPOVER type.)
 * @param [in] mirror If true, this is a mirror of a bidirectional arrowhead.
 *                    E.g., when having ->a<-, the left side is the mirror of the
 *                    right side. When we consider the mirror, we should omit any
 *                    symmetrical element at the front (but consider its offset).
 * @param [in] cache If the covers of the arrowhead were cached, supplying them will speed up operation.
 * @returns The contour of the dot or diamond.*/
Contour ArrowHead::TargetCover(const Path & path, const PathPos &pos, bool forward, bool flip,
                               const LineAttr & mainline_before, const LineAttr & mainline_after,
                               bool mirror [[maybe_unused]], const ArrowHeadCoverCache *cache) const
{
    _ASSERT(path.IsValidPos(pos));
    if (size()==0) return Contour();
    if (front().type!=ESingleArrowType::BOX &&
        front().type!=ESingleArrowType::DIAMOND &&
        front().type!=ESingleArrowType::DOT)
        return Contour(); //No other arrow type covers the entity line
    if (cache) {
        _ASSERT(pos == cache->first.tip_pos || mirror);
        return cache->first.cover;
    }
    if (!path.IsValidPos(pos)) return Contour();
    return front().Cover(path, pos, forward, flip,
                         mainline_before, mainline_after,
                         fsah(), lsah());
}

/** Returns an area where the arrow line should not be drawn.
 * This version of the function assumes the arrowhead is drawn to an
 * arbitrary position on an arbitrary path.
 * @param [in] path The arrowhead is along this path.
 * @param [in] pos The tip of the arrowhead is at this position.
 * @param [in] forward True if the arrowhead points towards the end of the path.
 * @param [in] flip If true, the arrowhead needs to be flipped to the arrow line.
 *                  useful for the 'half' style, which is not symmetric to the arrow line.
 * @param [in] ignore_opaque When set to true, we will not clip the line for filled arrowheads.
 *                           This may lead to better performance if you draw the arrowhead
 *                           *after* the arrow line and the fill color is opaque.
 * @param [in] mainline_before The line style of the arrow line from the direction
 *                             we point from (=before the target line). (Used by JUMPOVER type.)
 * @param [in] mainline_after The line style of the arrow line from the direction
 *                            we point to. (=after the target line) (Used by JUMPOVER type.)
 * @param [in] mirror If true, this is a mirror of a bidirectional arrowhead.
 *                    E.g., when having ->a<-, the left side is the mirror of the
 *                    right side. When we consider the mirror, we should omit any
 *                    symmetrical element at the front (but consider its offset).
 * @param [in] cache If the covers of the arrowhead were cached, supplying them will speed up operation.
 * @returns The contour where the arrow line shall not be drawn.*/
Contour ArrowHead::ClipForLine(const Path & path, const PathPos & pos, bool forward, bool flip, bool ignore_opaque,
                               const LineAttr & mainline_before, const LineAttr & mainline_after,
                               bool mirror [[maybe_unused]], const ArrowHeadCoverCache *cache) const
{
    Contour ret;
    _ASSERT(path.IsValidPos(pos));
    if (IsNone()) return ret;
    if (!path.IsValidPos(pos)) return ret;
    PathPos full = pos, half = pos;
    //SingleArrowHead::EClipLineType now = SingleArrowHead::CLIP_ALL;
    bool right = true;
    Contour add, subst;

    const double dir = forward ? -1 : +1; //for forward arrows we move backwards on the path with arrow series
    PathPos p = pos;
    for (unsigned u = 0; u<size(); u++) {
        auto who = at(u).WidthHeightOffset(mainline_before, u==0 ? mainline_after : mainline_before, fsah(), lsah());
        const auto t = at(u).clipline_type(flip, ignore_opaque, fsah(), lsah());
        if (t & SingleArrowHead::CLIP_ADD_COVER) {
            if (cache)
                add += u ? cache->further[u-1].cover : cache->first.cover;
            else
                add += at(u).Cover(path, p, forward, flip, mainline_before, mainline_after, fsah(), lsah());
        } else if (t & SingleArrowHead::CLIP_MINUS_COVER) {
            //if we are half only, we better deduct cover as if we were on both side
            //This avoids having a tiny part of the cover to remain after the subtraction
            //due to contour library impreciseness.
            if (at(u).side!=ESingleArrowSide::BOTH) {
                //Create a copy, where this element is ESingleArrowSide::BOTH
                ArrowHead ah(*this); //copy the entire series so that fsah() and lsah() are OK.
                ah.at(u).side = ESingleArrowSide::BOTH;
                subst += ah.at(u).Cover(path, p, forward, flip, mainline_before, mainline_after, ah.fsah(), ah.lsah());
            } else if (cache)
                subst += u ? cache->further[u-1].cover : cache->first.cover;
            else
                subst += at(u).Cover(path, p, forward, flip, mainline_before, mainline_after, fsah(), lsah());
        }
        double to_move = (u ? dir*who.after_x_extent : 0) + dir*who.offset;
        if (t & SingleArrowHead::CLIP_ONLY_FRONT) {
            if (path.MovePos(p, dir*who.offset/2))
                break; //too short path
            else
                to_move -= dir*who.offset/2;
        } else {
            if (path.MovePos(p, to_move))
                break; //too short path
            else
                to_move = 0;
        }
        switch (t & 3) {
        case SingleArrowHead::CLIP_ALL: //CLIP_ALL
            //_ASSERT(now==SingleArrowHead::CLIP_ALL);
            full = p;
            break;
        case SingleArrowHead::CLIP_RIGHT:
            //_ASSERT(now==SingleArrowHead::CLIP_ALL || now==SingleArrowHead::CLIP_RIGHT);
            half = p;
            //now = SingleArrowHead::CLIP_RIGHT;
            right = true;
            break;
        case SingleArrowHead::CLIP_LEFT:
            //_ASSERT(now==SingleArrowHead::CLIP_ALL || now==SingleArrowHead::CLIP_LEFT);
            half = p;
            //now = SingleArrowHead::CLIP_LEFT;
            right = false;
            break;
        case SingleArrowHead::CLIP_NO:
            //now = SingleArrowHead::CLIP_NO;
            break;
        }
        if (u+1<size())
            if (path.MovePos(p, to_move))
                break; //too short path
    }
    const double lw = std::max(mainline_before.LineWidth(), mainline_after.LineWidth())+0.5; //add this half pixel to surely cover line
    if (full!=pos)
        ret = Path(path.CopyPart(pos, full)).SimpleWiden(lw, EXPAND_MITER, EXPAND_MITER);
    if (half!=pos) {
        Path path2 = path.CopyPart(full, half);
        if (!right)
            path2.Invert();
        ret += path2.SimpleWidenAsymmetric(lw, 0, EXPAND_MITER, EXPAND_MITER);
    }
#ifdef _DEBUG
    if (0) {
        contour::debug::Snapshot(EC::STRINGFORMAT, "\\mn(1)", EC::START, "%n", EC::LINEWIDTH, 0.1, EC::MARKERWIDTH, 0.5,
                                 ColorType(255, 0, 0), ret[0].Outline(), ColorType(0, 0, 255), add[0].Outline(),
                                 ColorType(0, 128, 0), subst[0].Outline());
        std::string r = ret.Dump(true);
        std::string a = add.Dump(true);
        std::string s = subst.Dump(true);
    }
#endif
    ret += std::move(add);
    ret -= std::move(subst);
    return ret;
}

/** Returns a contour covering the arrowhead.
 * This version of the function assumes the arrowhead is drawn to an
 * arbitrary position on an arbitrary path.
 * @param [in] path The arrowhead is along this path.
 * @param [in] pos The tip of the arrowhead is at this position.
 * @param [in] forward True if the arrowhead points towards the end of the path.
 * @param [in] flip If true, the arrowhead needs to be flipped to the arrow line.
 *                  useful for the 'half' style, which is not symmetric to the arrow line.
 * @param [in] mainline_before The line style of the arrow line from the direction
 *                             we point from (=before the target line). (Used by JUMPOVER type.)
 * @param [in] mainline_after The line style of the arrow line from the direction
 *                            we point to. (=after the target line) (Used by JUMPOVER type.)
 * @param [in] mirror If true, this is a mirror of a bidirectional arrowhead.
 *                    E.g., when having ->a<-, the left side is the mirror of the
 *                    right side. When we consider the mirror, we should omit any
 *                    symmetrical element at the front (but consider its offset).
 * @param [out] cache If a non-nullptr cache object is given, it is filled with individual covers and
 *                    positions to be used by TargetCover(), ClipForLine() and Draw().
 *                    If the path is too short, the cache may end up being shorter than the series.
 *                    This will be tolerated by the users of the cache.
 *                    If mirror is true and we have a symmetric first element in the series,
 *                    the 'first' element of the cache will be unset - but further may contain subsequent
 *                    covers.
 *                    Whatever we get in this member we will destroy and compute afresh.
 * @returns The contour of the dot or diamond.*/
Contour ArrowHead::Cover(const Path & path, const PathPos & pos, bool forward, bool flip,
                         const LineAttr & mainline_before, const LineAttr & mainline_after,
                         bool mirror, ArrowHeadCoverCache *cache) const
{
    if (cache) {
        cache->further.clear();
        cache->first.cover.clear();
        cache->first.tip_pos = pos;
    }
    _ASSERT(path.IsValidPos(pos));
    if (!path.IsValidPos(pos)) return Contour();
    const double dir = forward ? -1 : +1; //for forward arrows we move backwards on the path with arrow series
    Contour ret;
    PathPos p = pos;
    for (unsigned u = 0; u<size(); u++) {
        auto who = at(u).WidthHeightOffset(mainline_before, u==0 ? mainline_after : mainline_before, fsah(), lsah());
        if (u)
            if (path.MovePos(p, dir*who.after_x_extent))
                break; //too short path
        if (!mirror || u || !::IsSymmetricAndNotNone(at(u).type)) {
            //skip the first element if symmetric and mirror==true
            Contour element;
            if (FAST_ARROWHEAD) {
                const XY tip = path.GetPoint(p);
                const XY tangent = path.GetTangent(p, !forward, false);
                const double dist = tangent.Distance(tip);
                _ASSERT(dist>0.0001);
                const double cos = (tip.x-tangent.x)/dist;
                const double sin = (tip.y-tangent.y)/dist;
                element = at(u).Cover(tip, flip, mainline_before, u==0 ? mainline_after : mainline_before, fsah(), lsah()).
                                  RotateAround(tip, cos, sin);
            } else
                element = at(u).Cover(path, p, forward, flip,
                                      mainline_before, u==0 ? mainline_after : mainline_before,
                                      fsah(), lsah());
#ifdef _DEBUG
            if (0) {
                contour::debug::Snapshot(EC::STRINGFORMAT, "\\mn(1)", EC::START, "%n", EC::LINEWIDTH, 0.1, EC::MARKERWIDTH, 0.5,
                                         ColorType(255, 0, 0), ret[0].Outline(), ColorType(0, 0, 255), element[0].Outline());
                std::string r = ret.Dump(true);
                std::string a = element.Dump(true);
            }
#endif
            if (cache) {
                ret += element;
                if (u==0) {
                    cache->first.cover.swap(element);
                    cache->first.tip_pos = p;
                } else {
                    cache->further.emplace_back();
                    cache->further.back().cover.swap(element);
                    cache->further.back().tip_pos = p;
                }
            } else {
                ret += std::move(element);
            }
        }
        if (u+1<size())
            if (path.MovePos(p, dir*who.offset))
                break; //too short path
    }
    return ret;
}

/** Actually draws the arrowhead to a canvas.
 * This version of the function assumes the arrowhead is drawn to an
 * arbitrary position on an arbitrary path.
 * @param canvas The canvas to draw onto.
 * @param [in] path The arrowhead is along this path.
 * @param [in] pos The tip of the arrowhead is at this position.
 * @param [in] forward True if the arrowhead points towards the end of the path.
 * @param [in] flip If true, the arrowhead needs to be flipped to the arrow line.
 *                  useful for the 'half' style, which is not symmetric to the arrow line.
 * @param [in] mainline_before The line style of the arrow line from the direction
 *                             we point from (=before the target line). (Used by JUMPOVER type.)
 * @param [in] mainline_after The line style of the arrow line from the direction
 *                            we point to. (=after the target line) (Used by JUMPOVER type.)
 * @param [in] mirror If true, this is a mirror of a bidirectional arrowhead.
 *                    E.g., when having ->a<-, the left side is the mirror of the
 *                    right side. When we consider the mirror, we should omit any
 *                    symmetrical element at the front (but consider its offset).
 * @param [in] cache If the covers of the arrowhead were cached, supplying them will speed up operation.
 * @returns The contour of the dot or diamond.*/
void ArrowHead::Draw(Canvas & canvas, const Path & path, const PathPos & pos, bool forward, bool flip,
                     const LineAttr & mainline_before, const LineAttr & mainline_after,
                     bool mirror, const ArrowHeadCoverCache *cache) const
{
    _ASSERT(path.IsValidPos(pos));
    if (!path.IsValidPos(pos)) return;
    if (cache) {
        _ASSERT(pos == cache->first.tip_pos || mirror);
        if (!mirror || !StartsWithSymmetricAndNotNone()) //consider first element only if non mirrored or non symmetrical
            front().Draw(canvas, path, cache->first.tip_pos, forward, flip,
                           mainline_before, mainline_after,
                           fsah(), lsah(), &cache->first.cover);
        for (unsigned u = 0; u<cache->further.size(); u++)
            series[u+1].Draw(canvas, path, cache->further[u].tip_pos, forward, flip,
                             mainline_before, mainline_before,
                             fsah(), lsah(), &cache->further[u].cover);
        return;
    }
    const double dir = forward ? -1 : +1; //for forward arrows we move backwards on the path with arrow series
    PathPos p = pos;
    for (unsigned u = 0; u<size(); u++) {
        auto who = at(u).WidthHeightOffset(mainline_before, u==0 ? mainline_after : mainline_before, fsah(), lsah());
        if (u)
            if (path.MovePos(p, dir*who.after_x_extent))
                break; //too short path
        if (!mirror || u || !::IsSymmetricAndNotNone(at(u).type)) {
            //skip the first element if symmetric and mirror==true
            if (FAST_ARROWHEAD) {
                const XY tip = path.GetPoint(p);
                const XY tangent = path.GetTangent(p, !forward, false);
                const double dist = tangent.Distance(tip);
                _ASSERT(dist>0.0001);
                const double cos = (tip.x-tangent.x)/dist;
                const double sin = (tip.y-tangent.y)/dist;
                canvas.Transform_Rotate(tip, cos, sin);
                at(u).Draw(canvas, tip, flip, mainline_before, u==0 ? mainline_after : mainline_before, fsah(), lsah());
                canvas.UnTransform();
            } else {
                at(u).Draw(canvas, path, p, forward, flip, mainline_before, u==0 ? mainline_after : mainline_before,
                           fsah(), lsah());
            }
        }
        if (u+1<size())
            if (path.MovePos(p, dir*who.offset))
                break; //too short path
    }
}


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

/** Cut away the end part of the path covered by our arrowhead.
 * Note: for the other functions (Draw, TargetCover, you still need to
 * submit the original, uncut path, so that we can know where is the
 * tip of the arrow.
 * Note that you have two choices. Either you draw the original path,
 * all the way to its tip, but clip it using ClipForLine(); or you
 * truncate the path and draw only a shorter version using this function.
 * The former draws nicer especially for thicker or double lines, but is
 * slower due to the clipping.
 * @param [in] path The path with the arrowhead at its end.
 * @param [in] line The line style of the path.
 * @returns the reduced path.*/
Path OneArrowHead::ClipLine(const Path & path, const LineAttr & line) const
{
    _ASSERT(!dirty);
    Path ret;
    PathPos pos = path.GetEndPos();
    if (path.MovePos(pos, -endArrowHead.WidthHeight(line, line).before_x_extent))
        return ret;
    ret = path;
    ret.SetEnd(pos);
    return ret;
}

/** Cut away the end part of the path covered by the arrowheads.
 * Note: for the other functions (Draw, TargetCover, you still need to
 * submit the original, uncut path, so that we can know where is the
 * tip of the arrow.
 * Note that you have two choices. Either you draw the original path,
 * all the way to its tip, but clip it using ClipForLine(); or you
 * truncate the path and draw only a shorter version using this function.
 * The former draws nicer especially for thicker or double lines, but is
 * slower due to the clipping.
 * @param [in] path The path with the arrowhead at its start and end.
 * @param [in] line The line style of the path.
 * @returns the reduced path.*/
Path TwoArrowHeads::ClipLine(const Path & path, const LineAttr & line) const
{
    _ASSERT(!dirty);
    Path ret;
    PathPos pos = path.GetEndPos();
    if (path.MovePos(pos, -endArrowHead.WidthHeight(line, line).before_x_extent))
        return ret;
    ret = path;
    ret.SetEnd(pos);
    pos = ret.GetStartPos();
    if (ret.MovePos(pos, startArrowHead.WidthHeight(line, line).before_x_extent))
        return Path();
    ret.SetStart(pos);
    return ret;
}


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

/** Returns a reference to the arrowhead that will be used to draw at one side
 * of a location.
 * This considers a whole arrow, including the two sides of middle arrowheads.
 * Say for 'a<->b<->c' we actually have draw two triangles around 'b', both using
 * midArrowHead. But for a->b->c, we draw only one triangle on the left side of 'b',
 * (with midArrowHead), but nothing on the right side. For the latter we return
 * FullArrowHeads::noneArrowHead. Note that two combinations (beyond start and end, such as
 * forward=left && which=start or forward!=left && which=end) are invalid, for those
 * we also return noneArrowHead.
 *
 * @param [in] forward True if arrow points from left to right, that is a->b and not a<-b.
 * @param [in] left True if we are interested in the arrow to draw to the left side of the entity line.
 *                  For symmetric types (dot) this gives the same irrespective of `left`.
 * @param [in] bidir True if the arrow is bi-directional, that is like a<->b and not a->b.
 * @param [in] which Tells us which end are we interested in (or middle).
 * @return The arrowhead to use. Can return none for start and end arrows (outside part) or
 *         arrowhead for middle arrows not to be drawn.*/
 const ArrowHead &FullArrowHeads::GetArrowHead(bool forward, bool bidir, EArrowEnd which, bool left) const noexcept
{
     _ASSERT(!dirty);
    const ArrowHead &ret = GetArrowHead(bidir, which);
    if (ret.IsSymmetricOrNone()) return ret;
    switch (which) {
    case EArrowEnd::START:
        return forward == left ? ArrowHead::noneArrowHead : ret;
    case EArrowEnd::MIDDLE:
        if (bidir) return ret;
        return forward == left ? ret : ArrowHead::noneArrowHead;
    case EArrowEnd::SKIP:
        if (bidir) return ret;
        return forward == left ? ret : ArrowHead::noneArrowHead;
    case EArrowEnd::END:
        return forward == left ? ret : ArrowHead::noneArrowHead;
    }
    _ASSERT(0);
    return ArrowHead::noneArrowHead;
}

 /** Returns what area the arrowhead covers.
 * @param [in] ah The ArrowHead this instance is associated with
 * @param [in] path The path with the arrowhead at its end.
 * @param [in] line The line style of the path.
 * @returns the area of coverage.*/
 Contour OneArrowHeadInstance::Cover(const OneArrowHead &ah, const Path & path, const LineAttr & line) const
 {
     if (!cache_computed) {
         ah.endArrowHead.Cover(path, true, line, line, true, false, &endArrowHeadCache);
         cache_computed = true;
     }
     Contour ret = endArrowHeadCache.first.cover;
     for (auto &c: endArrowHeadCache.further)
         ret += c.cover;
     return ret;
 }

 /** Returns what area the arrowheads cover.
 * @param [in] ah The ArrowHead this instance is associated with
 * @param [in] path The path with the arrowhead at its start and end.
 * @param [in] line The line style of the path.
 * @returns the area of coverage.*/
Contour TwoArrowHeadsInstance::Cover(const TwoArrowHeads &ah, const Path & path, const LineAttr & line) const
{
    if (!cache_computed) {
        ah.endArrowHead.Cover(path, true, line, line, true, false, &endArrowHeadCache);
        ah.startArrowHead.Cover(path, false, line, line, false, false, &startArrowHeadCache);
        cache_computed = true;
    }
    Contour ret = endArrowHeadCache.first.cover;
    for (auto &c: endArrowHeadCache.further)
        ret += c.cover;
    ret += startArrowHeadCache.first.cover;
    for (auto &c: startArrowHeadCache.further)
        ret += c.cover;
    return ret;
}
