/*
    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 chartbase.cpp The implementation of Chart, mainly the generic drawing part
* @ingroup libcgencommon_files  */


#define _CRT_SECURE_NO_DEPRECATE //For visual studio to allow sscanf
#define _CRT_NONSTDC_NO_DEPRECATE

#include "chartbase.h"
#include "cgen_progress.h"
#include "canvas.h"
#include "cgencommon.h"

/** Jumps over tabs and spaces in 's'. If 's' starts with none of these, nothing is done.
 * Any zero character stops processing.
 * @param s The string to observe.
 * @returns the string_view starting after the whitespace.*/
void DeserializeWhiteSpace(std::string_view &s)
{
    while (s.length() && (s.front()==' ' || s.front()=='\t'))
        s.remove_prefix(1);
}

/** Jumps a specific token (and any whitespace before).
 * Any zero character stops processing.
 * @param [in] s The string to observe.
 * @param [in] token The token to jump.
 * @returns true if the token is found and the string_view starting after the token,
 *          false if the token is not found and the original s (whitespace skipped).*/
bool DeserializeToken(std::string_view &s, std::string_view token)
{
    DeserializeWhiteSpace(s);
    if (s.substr(0, token.length()) != token)
        return false;
    s.remove_prefix(token.length());
    return true;
}


/** Jumps a newline (and any whitespace before).
 * Any zero character stops processing.
 * @param [in] s The string to observe.
 * @returns true if newline is found and the string_view starting after it,
 *          false if nothing or something else than a newline is not found and the original s (whitespace skipped).*/
bool DeserializeNewline(std::string_view &s)
{
    DeserializeWhiteSpace(s);
    if (s.length()) {
        if ('\x0d'==s.front()) {
            if (s.length()>1 && '\x0a'==s[1])
                s.remove_prefix(2);
            else
                s.remove_prefix(1);
        } else if ('\x0a'==s.front()) {
            s.remove_prefix(1);
        } else
            return false; //found something, but not newline
        return true;
    }
    return false; //found something, but not newline
}

/** Interprets a decimal number (and any whitespace before).
 * Any zero character stops processing.
 * @param s The string to observe.
 * @returns true if the int is found and the string_view starting after the token,
 *          false if the int is not found and the original s (whitespace skipped).*/
std::pair<bool, int64_t> DeserializeInt(std::string_view &s)
{
    DeserializeWhiteSpace(s);
    char *end;
    double d = strtod(s.data(), &end);
    if (end==s.data() || round(d)!=d)
        return {false, {}};
    s.remove_prefix(end-s.data());
    return {true, int64_t(d)};
}

/** Interprets a decimal floating point number (and any whitespace before).
 * Any zero character stops processing.
 * @param [in] s The string to observe.
 * @returns true if the number is found and the string_view starting after the token,
 *          false if the number is not found and the original s (whitespace skipped).*/
std::pair<bool, double> DeserializeDouble(std::string_view &s)
{
    DeserializeWhiteSpace(s);
    char *end;
    double d = strtod(s.data(), &end);
    if (end==s.data())
        return {false, {}};
    s.remove_prefix(end-s.data());
    return {true, d};
}

/** Interprets whitespace delimited decimal numbers (and any whitespace before).
 * Any zero character stops processing.
 * @param s The string to observe.
 * @param [in] maxnum stop after this many.
 * @returns as many integers we found and the string_view starting after them
 *          (whitespace skipped)*/
std::vector<int64_t> DeserializeInts(std::string_view &s, size_t maxnum)
{
    std::vector<int64_t> ret = {};
    while(ret.size()<maxnum) {
        DeserializeWhiteSpace(s);
        auto r = DeserializeInt(s);
        if (r.first)
            ret.push_back(r.second);
         else
            break;
    }
    return ret;
}

/** Interprets whitespace delimited double numbers (and any whitespace before).
 * Any zero character stops processing.
 * @param  s The string to observe.
 * @param [in] maxnum stop after this many.
 * @returns as many integers we found and the string_view starting after them
 *          (whitespace skipped)*/
std::vector<double> DeserializeDoubles(std::string_view &s, size_t maxnum)
{
    std::vector<double> ret = {};
    while(ret.size()<maxnum) {
        DeserializeWhiteSpace(s);
        auto r = DeserializeDouble(s);
        if (r.first)
            ret.push_back(r.second);
         else
            break;
    }
    return ret;
}

/** Takes chars until the next newline or end. Removes heading and trailing whitespace.
 * If newlinemandatory is true, we return false if the string_view ended before a newline was found.*/
std::pair<bool, std::string_view> DeserializeLine(std::string_view &s, bool newline_mandatory)
{
    std::pair<bool, std::string_view> ret = {true, {}};
    DeserializeWhiteSpace(s);
    const size_t end = std::min(s.find_first_of('\x0d'), s.find_first_of('\x0a'));
    if (end==std::string_view::npos) {
        if (newline_mandatory)
            ret.first = false;
        else {
            ret.second = s;
            s = {};
        }
    } else {
        ret.second = s.substr(0, end);
        s.remove_prefix(end);
        DeserializeNewline(s); //we may have a 2-char CRLF
    }
    return ret;
}



bool ProgressBase::DoCallback(double p) const
{
    bool stop = false;
    current_percent = p;
    if (callback &&
            (abs(p - last_reported)>granularity ||
            (last_time==clock::time_point::min() || (clock::now()-last_time>time_granularity)))) {
        last_reported = p;
        bool ret = callback(this, data, myPreferredAbortMethod);
        last_time = clock::now();
        stop = ret && myPreferredAbortMethod==RETVAL;
    }
    return stop;
}

const char LabelInfo::labelTypeChar[] = "-EADBbPVCSN";

std::unique_ptr<Csh> Chart::CshFactory(Csh::FileListProc, const LanguageCollection* languages) const
{
    return std::make_unique<DummyCsh>(languages);
}

/** Find what fill to apply in the range Y.We assume all fill blocks span the
 * entire chart width and only differ in their Y coordinate. We search for the fill block
 * covering 'y' (or is at least adjacent to it) and we synthetize something close.
 * This is needed because the footer (copyright text) is drawn generically and may be
 * at the middle of the chart (if at page boundary). Each individual language must
 * expose its background so that it can be drawn with the correct background.
 * @param [in] Y The y range we generate the fill for in chart coordinates
 * @param [in] top If true, then Y shall be synthetized from the fill at Y.till
 *             meaning the filled region is a header. If false it is a footer and the
 *             fill at Y.from shall be used to synthetize the result.
 *
 * If we do not have a bk fill registered for Y.from/till, we return a default white
 * fill. */
FillAttr Chart::GetBkFill(Range Y, bool top) const noexcept {
    //We do this simple: we always return a non-gradient fill.
    //We find the fill at the top or bottom of Y.
    const double y = top ? Y.till : Y.from;
    //if Y is at the boundary of two background fills, we shall select based on 'top'
    const BackgroundFill* best = nullptr, * good = nullptr;
    for (const BackgroundFill &f : bkFill)
        switch (f.area.y.IsWithin(y)) {
        default:
        case contour::WI_IN_HOLE:
        case contour::WI_OUTSIDE: continue;
        case contour::WI_INSIDE: best = good = &f; break;
        case contour::WI_ON_EDGE:
        case contour::WI_ON_VERTEX:
            good = &f;
            if ( top && f.area.y.from==y) best = &f;
            if (!top && f.area.y.till==y) best = &f; //two ifs handle the case of f.area.y.from==f.area.y.till
        }
    if (!best) best = good;
    if (!best) return {};
    if (contour::test_zero(best->area.y.Spans())) return best->fill;
    return best->fill.ApproxColorHorizontal(best->area.y.Pos(y));
}


Chart::Chart(FileReadProcedure *p, void *param, const LanguageCollection* languages) :
    first_include(-1),
    total(0, 0, 0, 0), copyrightTextHeight(0),
    noLabels(0), noOverflownLabels(0),
    file_read_proc(p), file_read_proc_param(param),
    languages(languages)
{
    ignore_designs = false;
    prepare_for_tracking = true;
    prepare_element_controls = false;
}


const char Chart::compass_points[][5] =
{ "n","ne","e","se","s","sw","w","nw","c","_", "perp", "" };

const double Chart::compass_point_degree[] =
{ 0, 45, 90, 135, 180, 225, 270, 315, -1, -1, -2 };


/** Parses a compass point 'sw', 'e', etc. Or a number in (0..360)
 * if 'report_error' is true we emit a message on an error.
 * We return the compass point in degrees, from north clockwise.
 * We return -1 if compass point is not set, center or '_'.
 * We return -2 on perpendicular to contour.*/
std::optional<double>
Chart::ParseCompassPoint(std::string_view n, const FileLineCol &l,
                         bool report_error)
{
    double ret;
    if (!from_chars(n, ret)) {
        if (ret<0 && ret>=-360) ret += 360;
        if (ret>=0 || ret<=360) return ret;
        if (report_error)
            Error.Error(l, "Compass points shall be between [-360..360]. Ignoring this one.");
        return {};
    }
    for (unsigned u = 0; compass_points[u][0]; u++)
        if (CaseInsensitiveEqual(n, compass_points[u]))
            return compass_point_degree[u];
    if (report_error)
        Error.Error(l, StrCat("Did not recognize '", n, "' as a compass point. Ignoring it."),
                    "Use one of 'nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'c' or 'perp'.");
    return {};
}

std::unique_ptr<Chart> Chart::CompileInlineText(std::string_view language, const FileLineColRange& attr_lang,
                                                std::string_view text, const FileLineCol& start, const Element* from,
                                                std::string_view gui_state) {
    std::unique_ptr<Chart> chart;
    if (language.empty())
        Error.Error(start, "Inlining a text of unspecified language.");
    else if (!languages)
        Error.Error(attr_lang.start, "You cannot inline here, languages not available.");
    else if (auto *l = languages->GetLanguage(language)) {
        chart = l->create_chart(file_read_proc, file_read_proc_param);
        chart->GetProgress()->active = false;
        chart->prepare_for_tracking = prepare_for_tracking;
        chart->prepare_element_controls = prepare_element_controls;
        chart->GetProgress()->callback = nullptr;
        if (forced_pedantic)
            chart->SetPedantic(*forced_pedantic);
        chart->Shapes = l->shapes;
        chart->ImportDesigns(l->designs);
        if (forced_design.size())
            chart->ApplyForcedDesign(forced_design);
        chart->SetDefaultFont(forced_fontface, forced_fontlang, forced_fontlocation);
        chart->SetPedantic(GetPedantic());
        chart->DeserializeGUIState(gui_state);
        //Copy the files we use so that they are the same in both Charts.
        //So that we can use the same FileLineCol values.
        //For this reason we skip any errors the design libs of the inlined
        //language contained: the designlib files of the inlined lang do not show up
        //(but the ones of 'this' do, but not referenced).
        chart->Error.Files = Error.Files;
        chart->inlined_from = from;
        chart->ParseText(text, start);
        Error.Files = chart->Error.Files; //Make sure the 'file' of all locations in 'chart' is valid in 'this'
        chart->CompleteParse(false, false, XY(0, 0), false, /*collect links*/true);
    } else
        Error.Error(attr_lang.start, StrCat("Unrecognized language '", language, "'."),
                    StrCat("Use one of ", languages->GetLanguageNames(), "."));
    return chart;
}



/** Draws the chart into one of more files.
* If the chart contains only one page, we create a single file. If there
* are multiple pages, we create one for each. If case of PDF/PPT output format
* if pageSize is nonzero, we create a single file with multiple pages.
* 'pageSize' can only be valid (positive `x` and `y`) if file format is PDF (which supports
* multiple pages). In that
* case `scale` can be <zero,zero> indicating that the chart should be fitted to page width.
* This is the only drawing function that can place an error into 'Error' if generateErrors is set.
* Scale contains a list of scales to try.
* @param [in] ot The format of output. Determines what type of an output file to create.
* @param [in] scale The scaling value requested by the user. Must be all-positive, if the
*                   margins are valid.
* @param [in] fn The name of the file to create. In case of multiple files, we append a number.
* @param [in] bPageBreak If true, we draw dashed lines for page break, when the
*                        whole chart is drawn in one.
* @param [in] ignore_pagebreaks If true, we draw a multi-page chart onto a single file
*                               (if pagesize is zero) or a single page (if pagesize is nonzero)
*                               ignoring page breaks.
* @param [in] include_chart_text If supplied and the image is a png image, we place the chart text
*                                into an iTXt chunk.
* @param [in] only_page If nonzero, we only print this page into the file specified by 'fn' (no
*                       page number added). In this case 'ignore_pagebreaks' is ignored.
* @param [in] pageSize If non-zero, a fixed page size is used and a single multi-page
*                      file will be created (only with PDF).
* @param [in] margins For fixed size pages, these are the margins
* @param [in] ha The horizontal alignment for fix size pages (-1: left, 0: center, +1:right)
* @param [in] va The vertical alignment for fix size pages (-1:up, 0: center, +1:bottom)
* @param [in] generateErrors If true and we cannot avoid overfill or have something the output file 
*                            cannot take (like PPT drawing inline charts with sheering), we generate a warning/error.
*                            Errors are also generated on file creation and similar hard errors.
* @returns False on file errors (but not on warnings or other drawing errors) irrespective of `generateErrors`. */
bool Chart::DrawToFile(Canvas::EOutputType ot, const XY &scale,
                       std::string_view fn, bool bPageBreak, bool ignore_pagebreaks,
                       const char *include_chart_text, unsigned only_page,
                       const XY &pageSize, const double margins[4],
                       int ha, int va, bool generateErrors)
{

    const PBDataVector pageBreakData = GetPageVector();
    const unsigned from = only_page ? only_page : pageBreakData.size()<=1 || ignore_pagebreaks ? 0 : 1;
    const unsigned till = only_page ? only_page : pageBreakData.size()<=1 || ignore_pagebreaks ? 0 : unsigned(pageBreakData.size());
    if (pageSize.x<=0 || pageSize.y<0)
        for (unsigned page = from; page<=till; page++) {
            std::string fileName(fn);
            if (page > 0 && only_page == 0) {  //append page number if not all pages written
                char num[3] = "00";
                num[0] = char('0' + page / 10);
                num[1] = char('0' + page % 10);
                fileName.insert(fn.find_last_of('.'), num);
            }
            if (ot==Canvas::ISMAP) {
                FILE *fout = fopen(fileName.c_str(), "wt");
                if (fout==nullptr) {
                    Error.FatalError(FileLineCol(0, 0), "Could not open output file '" + fileName + "'.");
                    return false;
                }
                Canvas canvas(Canvas::Empty::Query);
                CollectIsMapElements(canvas);
                for (auto &e : ismapData) {
                    if (page>0 && pageBreakData[page-1]->xy.y <= e.rect.x.from &&
                        pageBreakData[page]->xy.y > e.rect.x.from)
                        e.rect.Shift(XY(0, -pageBreakData[page-1]->xy.y));
                    const string s = e.Print();
                    fprintf(fout, "%s\n", s.c_str());
                }
                fclose(fout);
            } else {
                Canvas canvas(ot, total, copyrightTextHeight, fileName, scale, &pageBreakData, page);
                if (canvas.ErrorAfterCreation(generateErrors ? &Error : nullptr, &pageBreakData)) return false;
                if (include_chart_text)
                    canvas.AddiTXtChunk(EmbedChartData{ .chart_type = GetLanguageExtensions().front(), .chart_text = include_chart_text,
                                                        .chart_size = GetCanvasSize(), .gui_state=SerializeGUIState(), .page=page});
                DrawComplete(canvas, bPageBreak, page);
            }
        }
    else {
        _ASSERT(ot!=Canvas::ISMAP);
        _ASSERT(ot==Canvas::PDF || ot==Canvas::PPT); //only PDF/PPT supports multiple pages.
        Canvas canvas(ot, total, fn, scale, pageSize, margins, ha, va, copyrightTextHeight, from || till ? &pageBreakData : nullptr);
        if (canvas.ErrorAfterCreation(generateErrors ? &Error : nullptr, &pageBreakData)) return false;
        for (unsigned page = from; page<=till; page++) {
            DrawComplete(canvas, bPageBreak, page);
            if (page<till)
                if (!canvas.TurnPage(&pageBreakData, page+1, generateErrors ? &Error : nullptr))
                    return false;
        }
    }
    return true;
}

#ifdef CAIRO_HAS_WIN32_SURFACE
/** Draw the chart or one page of it to a metafile
* @param [in] ot The format of output. Determines what type of metafile we use.
*                can only be WMF, EMF or EMFWMF.
* @param [in] page The page to draw, zero for the whole chart.
* @param [in] bPageBreaks If true and we draw the whole chart and the chart has multiple
*                         pages, draw dashed lines for page breaks.
* @param [in] fallback_image_resolution Draw fallback images at this resolution.
* @param [out] metafile_size Return the size of the resultant metafile in bytes
* @param [out] fallback_images Return the area covered by fallback images (in chart space)
* @param [in] generateErrors If true and we cannot avoid overfill, we generate a warning.
*                            Errors are also generated on file creation and similar hard errors.
* @returns The EMF handle of the metafile created. */
HENHMETAFILE Chart::DrawToMetaFile(Canvas::EOutputType ot,
    unsigned page, bool bPageBreaks, double fallback_image_resolution,
    size_t *metafile_size, Contour *fallback_images, bool generateErrors)
{
    const PBDataVector pageBreakData = GetPageVector();
    _ASSERT(ot==Canvas::WMF || ot==Canvas::EMF || ot==Canvas::EMFWMF);
    if (ot!=Canvas::WMF && ot!=Canvas::EMF && ot!=Canvas::EMFWMF)
        return 0;
    if (page>0) bPageBreaks = false;
    Canvas canvas(ot, HDC(nullptr), total, copyrightTextHeight, XY(1, 1), &pageBreakData, page);
    if (canvas.ErrorAfterCreation(generateErrors ? &Error : nullptr, &pageBreakData)) return 0;
    if (fallback_image_resolution>0)
        canvas.SetFallbackImageResolution(fallback_image_resolution);
    DrawComplete(canvas, bPageBreaks, page);
    HENHMETAFILE ret = canvas.CloseAndGetEMF();
    if (fallback_images)
        *fallback_images = std::move(canvas.GetFallbackImagePlaces());
    if (metafile_size)
        *metafile_size = canvas.GetMetaFileSize();
    return ret;
}

/** Draw the chart or one page of it to a metafile or printer Device Context
* @param [in] ot The format of output. Determines what type of metafile we use.
*                can only be WMF, EMF, EMFWMF or PRINTER.
* @param hdc The DC to draw onto
* @param [in] scale Scale the chart by this amount
* @param [in] page The page to draw, zero for the whole chart.
* @param [in] bPageBreaks If true and we draw the whole chart and the chart has multiple
*                         pages, draw dashed lines for page breaks.
* @param [in] fallback_image_resolution Draw fallback images at this resolution.
* @param [in] generateErrors If true and we cannot avoid overfill, we generate a warning.
*                            Errors are also generated on file creation and similar hard errors.
* @returns The size of the metafile or zero at error.*/
size_t Chart::DrawToDC(Canvas::EOutputType ot, HDC hdc, const XY &scale,
    unsigned page, bool bPageBreaks,
    double fallback_image_resolution, bool generateErrors)
{
    _ASSERT(ot == Canvas::WMF || ot == Canvas::EMF ||
        ot == Canvas::EMFWMF || ot == Canvas::PRINTER);
    if (page>0) bPageBreaks = false;
    const PBDataVector pageBreakData = GetPageVector();
    Canvas canvas(ot, hdc, total, copyrightTextHeight, scale, &pageBreakData, page);
    if (canvas.ErrorAfterCreation(generateErrors ? &Error : nullptr, &pageBreakData)) return 0;
    if (fallback_image_resolution>0)
        canvas.SetFallbackImageResolution(fallback_image_resolution);
    DrawComplete(canvas, bPageBreaks, page);
    canvas.CloseOutput();
    return canvas.GetMetaFileSize();
}

#endif

/** Draw the whole chart (unscaled) to a cairo recording surface.
 * Draws all content in chart space with no translation, no heading, lefting and copyright.
 * @param [in] ot The format of output. Determines what type of approximations we use.
 * @param [in] bPageBreaks If true and the chart has multiple
 *                         pages, draw dashed lines for page breaks.
 * @param [in] generateErrors If true and we cannot avoid overfill, we generate a warning.
 *                            Errors are also generated on file creation and similar hard errors.
 * @returns The resultant cairo recording surface.
 */
cairo_surface_t *Chart::DrawToRecordingSurface(Canvas::EOutputType ot, bool bPageBreaks,
    bool generateErrors)
{
    cairo_surface_t *ret = cairo_recording_surface_create(CAIRO_CONTENT_COLOR_ALPHA, nullptr);
    cairo_status_t status = cairo_surface_status(ret);
    if (status != CAIRO_STATUS_SUCCESS) {
        cairo_surface_destroy(ret);
        return nullptr;
    }
    const PBDataVector pageBreakData = GetPageVector();
    Canvas canvas(ot, ret, total);
    if (canvas.ErrorAfterCreation(generateErrors ? &Error : nullptr, &pageBreakData)) {
        cairo_surface_destroy(ret);
        return nullptr;
    }
    DrawComplete(canvas, bPageBreaks, 0);
    return ret;
}

/** Replay the chart or one page of it onto a cairo recording surface using a version previously
* recorded via DrawToRecordingSurface().
* This function is used when the user views just one page.
* @param [in] full A recording surface onto which we previously recorded the drawing
*                  of the chart using DrawToRecordingSurface().
* @param [in] page The page to draw, zero for the whole chart.
* @param [in] generateErrors If true and we cannot avoid overfill, we generate a warning.
*                            Errors are also generated on file creation and similar hard errors.
* @returns The resultant cairo recording surface.
*/
cairo_surface_t *Chart::ReDrawOnePage(cairo_surface_t *full, unsigned page,
    bool generateErrors)
{
    cairo_surface_t *ret = cairo_recording_surface_create(CAIRO_CONTENT_COLOR_ALPHA, nullptr);
    cairo_status_t status = cairo_surface_status(ret);
    if (status != CAIRO_STATUS_SUCCESS) {
        cairo_surface_destroy(ret);
        return nullptr;
    }
    const PBDataVector pageBreakData = GetPageVector();
    Canvas canvas(Canvas::SVG, ret, total, copyrightTextHeight, XY(1., 1.), &pageBreakData, page);
    if (canvas.ErrorAfterCreation(generateErrors ? &Error : nullptr, &pageBreakData)) {
        cairo_surface_destroy(ret);
        return nullptr;
    }
    //GetContext() returns non-null after the check above
    cairo_set_source_surface(canvas.GetContext(), full, 0, 0);
    cairo_paint(canvas.GetContext());

    DrawHeaderFooter(canvas, page);
    return ret;
}

// Leaves ignore_designs and prepare_for_tracking intact.
// Leaves copyright text and its height intact.
// We also keep errors.
// but deletes shapes
void Chart::SetToEmpty()
{
    total = Block(0, 0, 0, 0);
    noLabels = 0;
    noOverflownLabels = 0;
    file_url.clear();
    file_info.clear();
    Shapes.clear();
    AllElements.clear();
    AllCovers.clear();
    labelData.clear();
    ismapData.clear();
    used_shapes.clear();
}

ElementRef Chart::FindElement(const XY& xy) const {
    const Area* pArea = AllCovers.InWhichFromBack(xy);
    if (!pArea || !pArea->arc) return {};
    if (auto* pData = pArea->arc->GetInlinedChartData(); pData && pData->chart) 
        if (auto ret = pData->chart->FindElement(XY(xy).Apply(pData->parent_to_inline))) {
            if (ret.inline_data)
                ret.inline_data->AddParent(*pData);
            else
                ret.inline_data.emplace(pData->parent_to_inline.Invert(), pData->chart.get());
            return ret;
        }
    return { pArea->arc, {} };
}

ElementRef Chart::FindElement(const FileLineCol& l) const {
    //in the map linenum_ranges are sorted by increasing length, we search the shortest first
    auto i = std::ranges::find_if(AllElements, [&l](auto& p) { return p.first.start <= l && l <= p.first.end; });
    if (i == AllElements.end() || !i->second)
        return {};
    if (auto* pData = i->second->GetInlinedChartData(); pData && pData->chart) {
        //Calculate the position inside the inlined text
        _ASSERT(pData->file_pos.start.line <= l.line);
        const size_t line = l.line - pData->file_pos.start.line + 1;
        const FileLineCol inside_inlining(l.file, line, 1 < line ? l.col : l.col - pData->file_pos.start.col + 1);
        if (auto ret = pData->chart->FindElement(inside_inlining)) {
            if (ret.inline_data)
                ret.inline_data->AddParent(*pData);
            else
                ret.inline_data.emplace(pData->parent_to_inline.Invert(), pData->chart.get());
            return ret;
        }
    }
    return { i->second, {} };
}


std::pair<const std::string*, FileLineCol>
Chart::Include(std::string_view filename, const FileLineCol &linenum_command)
{
    std::pair<const std::string*, FileLineCol> ret; //empty string, invalid file pos
    if (file_read_proc==nullptr) {
        Error.Error(linenum_command, "Cannot read include files in this setup. Ignoring include.");
    } else {
        auto r = file_read_proc(filename, file_read_proc_param, linenum_command.file>=0 ? Error.Files[linenum_command.file] : std::string_view{});
        if (std::get<0>(r).length()) {
            Error.Error(linenum_command, std::get<0>(r));
        } else {
            ret.first = &(included_files[std::get<1>(r)] = std::move(std::get<2>(r)));
            ret.second.file = Error.AddFile(std::get<1>(r).c_str());
            ret.second.line = ret.second.col = 1;
            if (first_include==-1)
                first_include = ret.second.file;
        }
    }
    return ret;
}

XY Chart::GetCanvasSize(unsigned page) const noexcept {
    const PBDataVector pages = GetPageVector();
    auto [origSize, origOffset, pbData] = Canvas::GetPagePosition(total, &pages, page);
    XY size = origSize + (pbData ? pbData->headingLeftingSize : XY(0, 0));
    size.y += copyrightTextHeight;
    return size;
}


/** Ensure that the attribute value specifies a color.
 *
 * If the attribute value is not a string that conforms to color specifications
 * (that is, not a name of a color and not an rgb value
 * triplet, etc., see ColorSet::GetColor()) we generate an error into `error` and
 * return false.
 * If the current context ignores colors/styles, we omit generating the error.
 * @param [in] a The attribute we test.
 * @returns True if the value specifies a color.*/
bool Chart::CheckColor(const Attribute & a)
{
    if (a.type==EAttrType::STRING) {
        ColorType c = GetCurrentContext()->colors.GetColor(a.value);
        if (c.type!=ColorType::INVALID) return true;
    }
    if (!GetCurrentContext()->SkipContent())
        Error.Error(a, true, "Unrecognized color name or definition: '" +
                    StringFormat::RemovePosEscapesCopy(a.value.c_str()) + "'. Ignoring attribute.");
    return false;
}
