/*
  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.h Definition of Chart and ChartBase - base classes for all charts
* @ingroup libcgencommon_files */


#ifndef CHARTBASE_H
#define CHARTBASE_H


#include <string>
#include <map>
#include <memory>
#include "error.h"
#include "element.h"
#include "canvas.h"
#include "cgen_shapes.h"
#include "cgen_progress.h"
#include "style.h"

/** Structure making parsing procedure definitions simpler.*/
template<class AttrList>
struct ProcDefParseHelper
{
    multi_segment_string          name;       ///<The name of the procedure. Must be there and non-empty for a valid definition.
    gsl::owner<ProcParamDefList*> parameters; ///<The parameter list of the procedure. Must not be null, even if no parameters.
    gsl::owner<AttrList*>         attrs;      ///<The attribute list of the definition (e.g., [export=yes]). May be null.
    gsl::owner<Procedure*>        body;       ///<The body. For a valid procedure we must have one status=OK
    bool                          had_error;
    FileLineCol                   linenum_name;
    FileLineCol                   linenum_body;
    ProcDefParseHelper() : parameters(nullptr), attrs(nullptr), body(nullptr), had_error(false) { name.init(); }
    ~ProcDefParseHelper() { name.destroy(); delete parameters; delete attrs; delete body; }
};


/** Holds coordinate info about one element or entity label.
 * Used to generate 'lmap' output.
 * Types are somewhat msc specific, but this is OK for now.*/
struct LabelInfo
{
    /** Describes what type of labels we can have */
    enum LabelType
    {
        INVALID = 0, ///<Not valid
        ENTITY,    ///<The label belongs to an entity header
        ARROW,     ///<The label belongs to an arrow
        DIVIDER,   ///<The label belongs to a divider (title, etc.)
        BOX,       ///<The label belongs to a box with content
        EMPTYBOX,  ///<The label belongs to an empty box
        PIPE,      ///<The label belongs to a pipe (segment)
        VERTICAL,  ///<The label belongs to a vertical (brace, etc.)
        COMMENT,   ///<The label belongs to a comment (side or end)
        SYMBOL,    ///<The label of symbol commands
        NOTE,      ///<The label belongs to a floating note
    };
    static const char labelTypeChar[NOTE+2];
    LabelType type; ///<The type of the element of the label
    string    text; ///<The text of the label
    Block     coord;///<The bounding box of the label (all lines), in chart coordinates, ignoring pagination
    LabelInfo(LabelType t, std::string_view s, const Block &b) :
        type(t), text(s), coord(b) {}
};

/** Data we store about a named element, that can be referenced via @\r(<name>) */
struct RefNameData
{
    string      number_text; ///<The number of the arc in text format (empty if none, set in PostParseProcess())
    FileLineCol linenum;     ///<Position of the value of the "refname" attribute used to name the arc (set in AddAttribute())
};

/** A set of design names containing a flag to show if they are full designs
 * (as opposed to partial).*/
using DesignNameCollection = std::map<std::string, bool>;

/** Struct to store the background fill for calculating the
 * background fill for headers and footers. */
struct BackgroundFill {
    FillAttr fill;  ///<The fill applied to the chart background
    Block area;     ///<The area the background fill is applied to in chart coordinates.
};

class LanguageCollection;

/** The base class for all charts.
 * It has facilities to describe the language, collect errors, shapes, chart elements
 * and their cover (for tracking), designs, supports pagination, copyright text,
 * references inside the chart and more.
 * It defines a three step processing model: ParseText() takes the input text and parses
 * it creating some (language dependent) internal data structure. ParseText() shall be
 * possible to be called several times (on several chunks of files). Design and shape
 * definitions must be fully collected during this phase, so ExportDesigns() shall be
 * possible to be called after ParseText() runs. Also, ImportDesigns() shall be possible
 * to be called before ParseText()s to emulate as if those designlib files were actually
 * parsed again to get the designs.
 * Second step is a call to CompleteParse(). This happens only once and shall prepare
 * everything that is required for parsing. This is the last place where you can emit
 * error or warning messages.
 * The final step is actual drawing. You have to override the DrawHeaderFooter() and
 * DrawComplete(), latter of which draws the complete chart (with a hint if only one page
 * is required) onto a Canvas, the former of which adds per page header and footer.
 * You also need to override RegisterAllLabels(), which shall go through all parsed elements
 * (after CompleteParse()) and register the location of all labels via RegisterLabel().
 * And also CollectIsMapElements() to be able to generate ISMAPs.
 */
class Chart
{
public:
    /** @name Language attributes
    * @{ */
    virtual std::string GetLanguageDescription() const = 0; ///<Returns the human-readable (UTF-8) short description
    virtual std::vector<std::string> GetLanguageExtensions() const = 0; ///<Returns a set of extensions in order of preference
    virtual std::string GetLanguageEntityName() const = 0; ///<How is the 'entity' called in this language
    virtual std::string GetLanguageDefaultText() const = 0; ///<Returns the default text of the chart in UTF-8
    virtual bool GetLanguageHasAutoheading() const = 0; ///<True if the chart has automatic headings
    virtual bool GetLanguageHasElementControls() const = 0; ///<True if the elements of the chart support GUI controls
    virtual std::unique_ptr<Csh> CshFactory(Csh::FileListProc proc, const LanguageCollection *languages) const; ///<Return the corresponding color syntax highlighter
    virtual std::map<std::string, std::string> RegisterLibraries() const { return {}; } ///<Return the libraries used (for the purpose of the GUI about box and commandline --version flag. In UTF-8, like "cairo v1.2.3". No need to register cairo, Msc will do that.

    /** @} */
public:
    /** Maps ranges of the input file to trackable Elements*/
    using LineToArcMapType = std::map<FileLineColRange, Element*, file_line_range_length_compare>;
    static const char compass_points[][5];
    static const double compass_point_degree[];

    /** @name Members holding chart content (entities, arcs, definitions, etc.)
    * @{ */
    MscError             Error;        ///<Collects error and warning messages
    std::string          file_url;     ///<Set by the file.url chart option
    std::string          file_info;    ///<Set by the file.info chart option
    ShapeCollection      Shapes;       ///<A set of named shapes. Each one was created via 'defshape' command.
    LineToArcMapType     AllElements;  ///<A map of input file ranges to trackable Element objects (essentially arcs and entity headings)
    AreaList             AllCovers;    ///<A set of arc contours with a pointer to the arcs. Used to identify the arc covering an XY coordinate.
    std::list<LabelInfo> labelData;    ///<Holds a catalogue of all labels with their coordinates. Filled by RegisterLabels().
    ISMap                ismapData;    ///<Holds a list of link information from \\L escapes. Filled by CollectIsMapElements().
    std::set<unsigned>   used_shapes;  ///<The id number of the shapes we used (for saving them in OLE objects)
    int                  first_include;///<The index of the first file in Error::Files that was included. -1 if no file was included. Design libraries do not count as file inclusion.
    std::map<std::string, std::string> included_files; ///<A set of included files mapping from filename (same as in Error.Files) to file content.
    /** @} */
protected:
    /** @name Members holding calculated information on chart geometry
    * @{ */
    Block  total;               ///<Total bounding box of the chart (minus copyright), in chart space, not considering pagination.
    double copyrightTextHeight; ///<Height of the copyright text
    std::vector<BackgroundFill> bkFill; ///<The list of background fills applied in the chart. Used to calculate the bkg of the header, footer. When empty, we default to white.
    unsigned noLabels;          ///<The number of labels (except entities)
    unsigned noOverflownLabels; ///<The number of labels overflown
    /** @} */
    FillAttr GetBkFill(Range Y, bool top) const noexcept;

    /** @name Members holding settings requested by the user
    * @{ */
    std::string forced_fontface;
    std::string forced_fontlang;
    FileLineColRange forced_fontlocation;
    std::string forced_design;
    std::optional<bool> forced_pedantic;
    const Element* inlined_from = nullptr; ///<If this chart is inlined from another chart, this is the inlining element in the parent chart
    /** @} */

    /** Parses the input We assume that the file has already been added to Error. 
     * 'first' char contains the position of the first character in 'input'.*/
    virtual void ParseText(std::string_view input, const FileLineCol &first_char) = 0;

public:
    /** A pointer to a procedure that reads this file and returns its content.
     * The second parameter is passed transparently.
     * Returns the error message ("" if OK) and the read text.
     * first returned element is error message ("" if OK), second is full UTF-8 path
     * of the file we have read, third is the file content.*/
    typedef std::tuple<std::string, std::string, std::string> FileReadProcedure(std::string_view filename, void *param, std::string_view included_from);

    Chart(FileReadProcedure *p, void *param, const LanguageCollection* languages);
    virtual ~Chart() = default;
    typedef std::unique_ptr<Chart>(*ChartFactoryFunction)(FileReadProcedure *p, void *param, const LanguageCollection* languages); ///<Function type to generate a chart
    FileReadProcedure * const file_read_proc;
    void * const file_read_proc_param;
    const LanguageCollection* const languages;

    /** Consumes a command-line argument specific to this chart type.
     * If the command-line argument is a general one, this function will not be called.
     * Return true if the argument was recognized for this language/chart type.
     * If command line arguments need a parameter, use the equal sign-format, so that
     * "-arg=param" and not "-arg param". All strings may be UTF-8.
     * If the parameter is not recognized and you return false here, but it starts
     * with a double hyphen, it is tested for a design (like "--omegapple") and applied,
     * if so. If no such design is available, or the parameter has an equal sign
     * AddCommandLineOption will be called.*/
    virtual bool AddCommandLineArg(const std::string& arg) = 0;
    /** Adds a command-line chart option. Shall emit all errors, as the callee
     * will not know if succeeded.*/
    virtual void AddCommandLineOption(const Attribute &a) = 0;
    /** Sets the default font and font language. Leave empty if none.
     * You does not have to call it, but if you do, do it right after the
     * creation of the chart object, before parsing.
     * For signalling charts and block diagrams, this is synonymous with
     * setting the text.font.{face,lang} chart options, for graphs it is
     * synonymous with issuing the {graph,node,edge} [ fontname = <face> ];
     * commands (and lang is ignored). If the font/face is not found or the
     * chart does not accept this setting, we silently return.*/
    virtual void SetDefaultFont(const std::string& face, const std::string& lang, const FileLineColRange& l) { forced_fontface = face; forced_fontlang = lang; forced_fontlocation = l; }

    /** Opens and loads a file for inclusion. Filename is UTF-8.
     * Returns the text to include and the position of its first character.*/
    virtual std::pair<const std::string*, FileLineCol> Include(std::string_view filename, const FileLineCol &linenum_command);

    /** Just before compiling a chart (after filling in the designs from the designlib) apply the forced design.
     * Returns false, if design not recognized.*/
    virtual bool ApplyForcedDesign(const string& name) { forced_design = name; return true; }
    /** Return the current state of the pedantic chart option at the outermost scope.
     * Shall be used after compilation.*/
    virtual bool GetPedantic() const noexcept = 0;
    /** Set the initial value of the pedantic chart option.
     * Shall be used before calling ParseText().*/
    virtual void SetPedantic(bool pedantic) noexcept { forced_pedantic = pedantic; }
    /** Returns the total bounding box of the chart (minus copyright), in chart space, not considering pagination.*/
    const Block &GetTotal() const noexcept { return total; }
    /** Returns the height of the copyright at the bottom.*/
    double GetCopyrightTextHeight() const noexcept { return copyrightTextHeight; }
    /** Get the required canvas size to draw a page (of the whole chart if page==0)
     * The returned size is in chart units.*/
    XY GetCanvasSize(unsigned page = 0) const noexcept;

    /** @name Get page, context and design data
     * @{ */
    /** Get the pointers to the pagination information of us. Valid after CompleteParse().*/
    virtual PBDataVector GetPageVector() const = 0;
    /** Get the number of pages. Valid after CompleteParse().*/
    virtual unsigned GetPageNum() const = 0;
    /** Get page information for a specific page. Valid after CompleteParse().*/
    virtual const PageBreakData *GetPageData(unsigned page) const = 0;
    /** Get the list of designs. Valid after ParseText().*/
    virtual DesignNameCollection GetDesignNames() const = 0;
    /** Export design data in a format understood by our ImportDesign(). Valid after ParseText().*/
    virtual ContextPtrCatalog ExportDesigns() const = 0;
    /** Set the list of designs. Valid even before ParseText().*/
    virtual void ImportDesigns(const ContextPtrCatalog&) = 0;
    /** Get the list of design names. Valid after ParseText().*/
    virtual std::string GetDesignNamesAsString(bool full) const = 0;
    /** Get the current context. Valid before or during ParseText().*/
    virtual const Context* GetCurrentContext() const = 0;
    bool CheckColor(const Attribute &a);
    /** True if we (based on the current context) are currently re-parsing a procedure definition (due to it being called)*/
    bool Reparsing() const { return GetCurrentContext() ? GetCurrentContext()->Reparsing() : false;  }
    /** True if we (based on the current context) are parsing a proc definition do not check names.*/
    bool SkipContent() const { return GetCurrentContext() ? GetCurrentContext()->SkipContent() : false; }
    /** Look up a procedure by name. Used during parsing and we search the whole context stack.*/
    virtual const Procedure *GetProcedure(std::string_view name) const = 0;
    /** Look up a parameter or variable by name. Used during parsing and we search the context stack:
     * - for parameters down to and including the context defining or replaying a procedure;
     * - for variables all the way.*/
    virtual ProcParamResolved *GetParameter(std::string_view name) = 0;
    /** Get a particular reference. Valid after ParseText().*/
    virtual const RefNameData *GetReference(std::string_view refname) const = 0;
    /** Get value of the "width" chart option - this is msc specific, -1 if none. Valid after ParseText().*/
    virtual double GetWidthAttr() const { return -1; } //msc specific
    /** Deserialize the GUI state (expand/collapse attributes) from a string. Empty string always ok and means no GUI exceptions.*/
    virtual bool DeserializeGUIState(std::string_view) { return true; }
    /** Serialize the GUI state (expand/collapse attributes) to a string. */
    virtual std::string SerializeGUIState() const { return string(); }
    /** Set the GUI state according to a clicked control. Returns true if a change has been made.*/
    virtual bool ControlClicked(Element *, EGUIControlType) { return false; }
    /** Notify our parent chart (if any) when our GUI state changes.*/
    void GUIStateChanged() { if (inlined_from) inlined_from->InlinedChartGUIStateChange(); }
    /** @} */

    /** @name Parse Options
    * @{ */
    bool ignore_designs;         ///<Ignore design changes
    bool prepare_for_tracking;   ///<If true, all elements shall compute an 'area' member, we fill 'AllCovers' and 'AllArcs'.
    bool prepare_element_controls;///<If true, We collect all signatures to force_collapse_box. Used if we need to do box collapses from a GUI.
    std::string copyrightText;   ///<The copyright text we display at the bottom of each page.
    double trackFrameWidth = 4;  ///<Width of the frames used for tracking boxes and pipes on screen
    double trackExpandBy = 2;    ///<How much do we expand tracking covers
    /** @} */

    /** Adds the list of chart options to a Csh object. */
    static void AttributeNames(Csh &, bool /*designOnly*/) {}
    /** Adds the list valid values for a chart options to a Csh object. */
    static bool AttributeValues(std::string_view /*attr*/, Csh &/*csh*/) { return true; }
    virtual ProgressBase *GetProgress()=0;   ///<Get the element tracking our progress

    /** Parses a compass point to a degree.*/
    std::optional<double> ParseCompassPoint(std::string_view n, const FileLineCol &l,
                                            bool report_error);

    /** Compiles some in-lined text of another language.
    * @param [in] language The language of the inlined text as defined in LanguageCollection.
    *             May be empty, which will result in an error and empty return.
    * @param [in] attr_lang The location of the language above. Invalid if 'language' is empty.
    * @param [in] text The inlined chart text of 'language' language.
    * @param [in] start The location of the first character of 'text'. File indices uses that of 'this->FileNames'.
    * @param [in] from The Element that inlines the chart. It is stored into the returned chart.
    * The chart text is parsed and CompleteParse() is also called, thus full layout is generated. The Errors
    * are left in the chart, the caller can merge them to this->Error. The GUI state of the created chart is final
    * and can be queried (or changed by ControlClicked().)
    * @returns A chart if parsing is successful (may contain errors). The FileNames are copied from 'this', but
    *          otherwise all locations start from line:col of 1:1.*/
    std::unique_ptr<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);

    /** Parse a piece of input text as a new input file.
     * This must be possible to be called several times, and must keep in-parse state
     * ready to accept new blocks of top-level syntax. (We will not break
     * top level blocks or instructions into two files, but may parse
     * several design libs in one go, and then the user input after; or this may be
     * used later to implement \#include-like behavior.
     * It must maintain a ready copy of 'Shapes' and 'ChartBase::Designs'.
     * @param [in] input string of UTF8 encoded chart text
     * @param [in] filename The UTF8 name of the file to use in error messages.
     * @returns the number of the file as will appear in FileLineCol errors.*/
    unsigned ParseText(std::string_view input, std::string_view filename) {
        unsigned ret = Error.AddFile(filename);
        ParseText(input, FileLineCol{(int)ret, 1, 1});
        return ret;
    }

    /** Called after all blocks of input text has been fed nto ParseText().
     * It must prepare the chart for drawing.
     * It must fill out the following fields (besides all chart-specific data)
     * total, copyrightTextHeight - these MUST be properly set in any case
     * pageBreakData - at least 1 correct element (having all the chart) must be added.
     * noLabers, noOverflownLabels - if we use text reflow and want to report overflows (leaving them 0 is OK)
     * used_shapes - if shapes are used (else OLE embedding will miss them) (leaving it to empty is OK)
     * ismapData - if requests by the caller (else we will not generate ismaps even if the user wanted them) (leaving it to empty is OK)
     * AllElements, AllCovers - if one wishes tracking (leaving it to empty is OK)
     * file_url, file_info - if one has these chart options (leaving it to empty is OK)*/
    virtual void CompleteParse(bool autoPaginate = false,
                               bool addHeading = true, XY pageSize = XY(0, 0),
                               bool fitWidth = true, bool collectLinkInfo = false) = 0;

    /** Used to count labels. Call once for every label and indicate if it has been overflowing.*/
    void CountLabel(bool overflown) { noLabels++; if (overflown) noOverflownLabels++; }
    //void RegisterLabel(const Label &l, LabelInfo::LabelType type, const Block &b);
    /** Register a horizontally drawn label (with slant perhaps).
    * This is used to generate lmaps
    * if angle is nonzero, sx, dx, cx and y are interpreted in the rotated space.
    * @param [in] l The label to register.
    * @param [in] type The type of element the label belongs to.
    * @param [in] sx The left margin.
    * @param [in] dx The right margin.
    * @param [in] y The top of the label.
    * @param [in] cx If also specified, we center around it for centered lines,
    *                but taking care not to go outside the margins.
    *                If the line is wider than `dx-sx` we will go outside
    *                as little as possible (thus we center around `(sx+dx)/2`.
    * @param [in] c The center of rotation in case of a slanted arrow label.
    * @param [in] angle The degrees of rotation in case of a slanted arrow label.*/
    void RegisterLabel(const Label &l, LabelInfo::LabelType type,
                       double sx, double dx, double y, double cx = -CONTOUR_INFINITY, const XY &c=XY(), double angle=0)
    { if (l.size()) labelData.emplace_back(type, std::string(l), l.Cover(Shapes, sx, dx, y, cx).RotateAround(c, angle).GetBoundingBox());}
    /** Register a vertically drawn label.
    * This is used to generate lmaps
    * @param [in] l The label to register.
    * @param [in] type The type of element the label belongs to.
    * @param [in] s The left edge of the text (as the text reads)
    *               In the result this is the Left/Top/Bottom edge of text for side==END/LEFT/RIGHT.
    * @param [in] d The right edge of the text (as the text reads)
    *               In the result this is the Right/Bottom/Top edge of text for side==END/LEFT/RIGHT
    * @param [in] t The top edge of the text (as the text reads)
    *               In the result this is the Top/Right/Left edge of text for side==END/LEFT/RIGHT.
    * @param [in] side from which direction is the text read. For END it will be laid out horizontally.
    * @param [in] c If also specified, we center around it for centered lines,
    *                but taking care not to go outside the margins.
    *                If the line is wider than `d-s` we will go outside
    *                as little as possible (thus we center around `(s+d)/2`.*/
    void RegisterLabel(const Label &l, LabelInfo::LabelType type,
                       double s, double d, double t, ESide side, double c = -CONTOUR_INFINITY)
    { if (l.size()) labelData.emplace_back(type, std::string(l), l.Cover(Shapes, s, d, t, side, c).GetBoundingBox());}

    /** Register a label (with arbitrary cover).
    * This is used to generate lmaps
    * @param [in] l The label to register.
    * @param [in] type The type of element the label belongs to.
    * @param [in] cover The bounding box pre calculated cover of the label.*/
    void RegisterLabel(const Label &l, LabelInfo::LabelType type, const Block &cover)
    { if (l.size()) labelData.emplace_back(type, std::string(l), cover);}

    /** Collect all link elements into 'ismapData'. To be called during or after CompleteParse().*/
    virtual void CollectIsMapElements(Canvas &canvas) = 0;
    /** Collect info on all labels into 'labelData'. To be called during or after CompleteParse().*/
    virtual void RegisterAllLabels() = 0;

    /** Draws one complete chart page or all pages.
    * We do not draw header, footer, only the chart content.
    * We do not fiddle with canvas preparation (assume it is prepared for our page), leave it as is.
    * We also assume that the draw section of progress has been started and we do not end it either.
    * We assume the page is a sane number.
    * @param canvas The canvas to draw on
    * @param [in] pageBreaks Governs if we draw page break indicators for multi-page charts.
    * @param [in] page The page to draw. Zero means the whole chart. */
    virtual void Draw(Canvas& canvas, bool pageBreaks, unsigned page) = 0;
    /** Draw the header and footer for a page.
    * This means a potential automatic heading and the copyright text.
    * This method expects the canvas to be 'prepared' and unprepares it.
    * @param canvas The canvas to draw on
    * @param [in] page The page to draw for. Zero means the whole chart. */
    virtual void DrawHeaderFooter(Canvas &canvas, unsigned page) = 0;
    /** Draws one complete chart page or all pages.
    * We draw header, footer and all chart content.
    * Expects the canvas to be 'prepared' and will un-prepare it.
    * @param canvas The canvas to draw on
    * @param [in] pageBreaks Governs if we draw page break indicators for multi-page charts.
    * @param [in] page The page to draw. Zero means the whole chart. */
    virtual void DrawComplete(Canvas& canvas, bool pageBreaks, unsigned page) = 0;

    virtual bool DrawToFile(Canvas::EOutputType, const XY& scale,
                            std::string_view fn, bool bPageBreak, bool ignore_pagebreaks,
                            const char* include_chart_text = nullptr, unsigned only_page = 0,
                            const XY &pageSize=XY(0,0), const double margins[4]=nullptr,
                            int ha=-1, int va=-1, bool generateErrors=false);
#ifdef CAIRO_HAS_WIN32_SURFACE
    virtual HENHMETAFILE DrawToMetaFile(Canvas::EOutputType, unsigned page, bool bPageBreaks,
                                        double fallback_image_resolution=-1, size_t *metafile_size=nullptr,
                                        Contour *fallback_images=nullptr, bool generateErrors=false);
    virtual size_t DrawToDC(Canvas::EOutputType ot, HDC hdc, const XY &scale,
                            unsigned page, bool bPageBreaks,
                            double fallback_image_resolution=-1,
                            bool generateErrors=false);
#endif
    virtual cairo_surface_t *DrawToRecordingSurface(Canvas::EOutputType,
        bool bPageBreaks, bool generateErrors = false);
    virtual cairo_surface_t *ReDrawOnePage(cairo_surface_t *full,
        unsigned page, bool generateErrors = false);
    /** Clear all internal data structures and result in an empty chart as if we have parsed an empty file. */
    virtual void SetToEmpty();

    /** Returns the element occupying 'xy'.
     * If the element holds an inlined chart, we search in that, too.
     * @param [in] xy The coordinate to look for in chart space.
     * @returns The element found or nullptr, if none. If the element returned is in an inlined chart,
     * we also return a transformation matrix to be inverted and applied to its areas so that they appear in the
     * chart space of 'this'; and the beginning of the inlining (to which we need to add the 'file_pos'
     * of the returned element.*/
    ElementRef FindElement(const XY& xy) const;
    /** Returns the element occupying a location in the input file.
     * If the element holds an inlined chart, we search in that, too.
     * @param [in] l The location in input files.
     * @returns The element found or nullptr, if none. If the element returned is in an inlined chart,
     *             we also return additional data. */
    ElementRef FindElement(const FileLineCol& l) const;
};

/** A template helping Chart descendants with useful functions.
 * ChartBase<> can operate on styles, progress tracker, page and reference data specific to the
 * given chart type.
 * 'Ctx' must be a descendant of ContextBase<>, 'Prog' must be a descendant of ProgressBase,
 * 'RefData' must be a descendant of RefNameData,
 * and 'Pagedata' must be a descendant of PageBreakData. By supplying your own classes
 * for these you can customize the pagination, referencing and/or style behavior
 * (Context is really a set of active styles).
 * This class can actually collect and hold designs, references and pagination data.*/
template <class Ctx = ContextBase<Style>, class Prog = ProgressBase, class RefData = RefNameData, class PageData = PageBreakData>
class ChartBase : public Chart
{
public:
    ChartBase(FileReadProcedure *p, void *param, const LanguageCollection* languages) : Chart(p, param, languages) {}

    /** @name Members holding chart content (entities, arcs, definitions, etc.)
    * @{ */
    Prog                                   Progress;      ///<The element tracking our progress
    std::list<Ctx>                         Contexts;      ///<A stack of ContextBase objects used during parsing
    std::map<string, Ctx, std::less<>>     Designs;       ///<A set of named designs. Each one was created via a `defdesign` command, except `plain`.
    std::map<string, RefData, std::less<>> ReferenceNames;///<A set of named arcs. Each one was named via the 'refname' attribute.
    std::vector<PageData>                  pageBreakData; ///<Starting xy pos and (later) other info for each page. pageBreakData[0].xy is always 0,0.
    /** @} */

    virtual ProgressBase *GetProgress() override { return &Progress; }

    /** @name Get context and design data
    * @{ */
    unsigned GetPageNum() const override { return unsigned(pageBreakData.size()); }
    const PageBreakData *GetPageData(unsigned page) const override { return page<pageBreakData.size() ? &pageBreakData[page] : nullptr; }
    PBDataVector GetPageVector() const override;
    std::map<std::string, bool> GetDesignNames() const override;
    std::string GetDesignNamesAsString(bool full) const override;
    ContextPtrCatalog ExportDesigns() const override; ///<Creates a copy of all designs
    void ImportDesigns(const ContextPtrCatalog&) override; ///<Fills in the Designs member with the content of this.
    const Context* GetCurrentContext() const override  { return Contexts.size() ? &Contexts.back() : nullptr; }
    const RefNameData *GetReference(std::string_view refname) const override;
    Ctx &MyCurrentContext() { return dynamic_cast<Ctx&>(Contexts.back()); }
    const Ctx &MyCurrentContext() const { return dynamic_cast<const Ctx&>(Contexts.back()); }
    /** Push a new, fresh context to the context stack.
     * @param [in] l The line of definition for the new context. The location of the first '{', e.g.;
     * @param [in] p The parse mode of the new context.
     * @param [in] t CLEAR will result in a totally empty context.
     *               EMPTY will have all default styles (but those empty).
     *               PLAIN will have the plain default design.*/
    void PushContext(const FileLineCol &l, EContextParse p, EContextCreate t)
        { Contexts.emplace_back(t!=EContextCreate::CLEAR, p, t, l); }
    /** Duplicates the last context on the stack, potentially changing the parse mode.
     * Used when switching to the not selected branch of an ifthenelse or replaying a procedure.*/
    void PushContext(const FileLineCol &l, EContextParse p)
        { auto &last = Contexts.back(); Contexts.emplace_back(last, p, l); }
    /** Duplicates the last context on the stack. Used when a user opens a new context via a '{'.*/
    void PushContext(const FileLineCol &l)
        { auto &last = Contexts.back(); Contexts.emplace_back(last, last.GetParseMode(), l); }
    const Procedure *GetProcedure(std::string_view name) const override;
    ProcParamResolved *GetParameter(std::string_view name) override;
    void SetVariable(ProcParamDef &&p, const FileLineColRange &def);
    void SetVariable(gsl::owner<ProcParamDef*> p, const FileLineColRange &def)
        {if (p) {SetVariable(std::move(*p), def); delete p;}}
    void PopContext();
    /** @} */

    /** Sets the font in all contexts and all full designs. */
    void SetDefaultFont(const std::string& face, const std::string& lang, const FileLineColRange&) override;
    /** Adds an attribute list to each of a list of styles.*/
    void AddAttributeListToStyleList(const AttributeList &al, const std::vector<std::string> &styles);
    /** Adds an attribute list to each of a list of styles.*/
    void AddAttributeListToStyleList(gsl::owner<AttributeList *>al, const std::vector<std::string> *styles)
        {if (al && styles) AddAttributeListToStyleList(*al, *styles); delete al; }
    /** Draws page breaks (dotted or dashed lines) */
    virtual void DrawPageBreaks(Canvas &canvas);
    void DrawHeaderFooter(Canvas &canvas, unsigned page) override;
    void DrawComplete(Canvas& canvas, bool pageBreaks, unsigned page) override {
        //Simple default implementation. Add yours to handle progress
        if (pageBreakData.size() == 0 || page > pageBreakData.size() || total.x.Spans() <= 0 || total.y.Spans()<=0) return;
        Draw(canvas, pageBreaks, page);
        DrawHeaderFooter(canvas, page);
    }
    void SetToEmpty() override;
};

template <class Ctx, class Prog, class RefData, class PageData>
PBDataVector ChartBase<Ctx, Prog, RefData, PageData>::GetPageVector() const
{
    PBDataVector ret;
    for (auto &d : pageBreakData)
        ret.push_back(&d);
    return ret;
}

template <class Ctx, class Prog, class RefData, class PageData>
std::map<std::string, bool> ChartBase<Ctx, Prog, RefData, PageData>::GetDesignNames() const
{
    std::map<std::string, bool> ret;
    for (auto &d : Designs)
        ret[d.first] = d.second.IsFull();
    return ret;
}

/** Returns a space delimited string of all full or partial design names.*/
template <class Ctx, class Prog, class RefData, class PageData>
std::string ChartBase<Ctx, Prog, RefData, PageData>::GetDesignNamesAsString(bool full) const
{
    string retval;
    for (auto &design : Designs)
        if (design.second.IsFull() == full) {
            if (retval.length())
                retval.append(" ");
            retval.append(design.first);
        }
    return retval;
}

template <class Ctx, class Prog, class RefData, class PageData>
ContextPtrCatalog ChartBase<Ctx, Prog, RefData, PageData>::ExportDesigns() const
{
    ContextPtrCatalog ret;
    for (auto &design : Designs)
        ret[design.first] = std::make_shared<Ctx>(design.second);
    return ret;
}

template <class Ctx, class Prog, class RefData, class PageData>
void ChartBase<Ctx, Prog, RefData, PageData>::ImportDesigns(const ContextPtrCatalog &cat)
{
    Designs.clear();
    for (auto &d : cat) {
        const Ctx *p = dynamic_cast<const Ctx *>(d.second.get());
        _ASSERT(p);
        if (p)
            Designs.emplace(d.first, *p);
    }
}

template <class Ctx, class Prog, class RefData, class PageData>
const RefNameData *ChartBase<Ctx, Prog, RefData, PageData>::GetReference(std::string_view refname) const
{
    auto i = ReferenceNames.find(refname);
    if (i==ReferenceNames.end())
        return nullptr;
    const RefData *ret = &i->second;
    const RefNameData *ret2 = ret;
    return ret2;
}

template <class Ctx, class Prog, class RefData, class PageData>
const Procedure *ChartBase<Ctx, Prog, RefData, PageData>::GetProcedure(std::string_view name) const
{
    for (auto i = Contexts.rbegin(); !(i==Contexts.rend()); i++) {
        auto p = i->Procedures.find(name);
        if (p!=i->Procedures.end())
            return &p->second;
    }
    return nullptr;
}

template <class Ctx, class Prog, class RefData, class PageData>
ProcParamResolved* ChartBase<Ctx, Prog, RefData, PageData>::GetParameter(std::string_view name)
{
    //We accept a parameter with the searched name only if we are
    //parsing or replaying a procedure. Else only variables play.
    bool accept_params = SkipContent() || Reparsing();
    for (auto i = Contexts.rbegin(); !(i==Contexts.rend()); i++) {
        auto p = i->parameters.find(name);
        if (p!=i->parameters.end()) {
            //We return the found name if we accept parameters or it is a variable
            if (accept_params || !p->second.is_parameter)
                return &p->second;
            else
                return nullptr;
        }
        //Stop if we hit a context that either defines or replays a procedure
        //(we do not want to match a parameter enclosing us)
        if (i->starts_procedure)
            accept_params = false;
    }
    return nullptr;
}

/** Adheres to a "set var=value" command, changing the value of 'var'.
 * 'var' can be a variable (new or existing) or a parameter.
 * The changing of the value is immediate and this should be called during parsing.
 * @param p The parsed ProcParamDef. We take ownership. If null, we no-op.
 *          If it has no value, we give an error and no-op.
 * @param def The location of the entire set command. Used to give error on missing value.*/
template<class Ctx, class Prog, class RefData, class PageData>
inline void ChartBase<Ctx, Prog, RefData, PageData>::SetVariable(ProcParamDef &&p, const FileLineColRange &def)
{
    if (p.has_default_value) {
        ProcParamResolved* r = GetParameter(p.name);
        if (r) {
            r->value = p.default_value;
            r->linenum_value = p.linenum_default_value;
        } else {
            MyCurrentContext().parameters.emplace(std::piecewise_construct,
                                                  std::forward_as_tuple(std::move(p.name)),
                                                  std::forward_as_tuple(std::move(p.default_value), p.linenum_default_value, false));
        }
    } else
        Error.Error(def.end, "Missing value.");
}

template<class Ctx, class Prog, class RefData, class PageData>
inline void ChartBase<Ctx, Prog, RefData, PageData>::PopContext()
{
    if (Contexts.size()>1) {
        auto &next = *++Contexts.rbegin();
        if (MyCurrentContext().export_colors)
            next.colors += std::move(MyCurrentContext().colors);
        if (MyCurrentContext().export_styles)
            next.styles += std::move(MyCurrentContext().styles);
    }
    Contexts.pop_back(); //auto-deletes the context via shared_ptr
}

template<class Ctx, class Prog, class RefData, class PageData>
inline void ChartBase<Ctx, Prog, RefData, PageData>::SetDefaultFont(const std::string& face, const std::string& lang, const FileLineColRange& l) {
    //We need to set the font in the current context, but also in all full designs
    //(even if setting it) - so that if the design is used later, we have this font
    if (face.length()) {
        for (Ctx& context : Contexts)
            context.text.face = face;
        for (auto& [name, design] : Designs)
            if (design.IsFull())
                design.text.face = face;
    }
    if (lang.length()) {
        for (Ctx& context : Contexts)
            context.text.lang = face;
        for (auto& [name, design] : Designs)
            if (design.IsFull())
                design.text.lang = face;
    }
    Chart::SetDefaultFont(face, lang, l);
}

template <class Ctx, class Prog, class RefData, class PageData>
void ChartBase<Ctx, Prog, RefData, PageData>::AddAttributeListToStyleList(
        const AttributeList &al, const std::vector<std::string> &sl) {
    if (al.size())
        for (auto &a : al) {
            std::vector<string> problem;
            bool had_generic = false;
            for (auto &s : sl) {
                auto style = MyCurrentContext().styles.GetStyle(s); //may be the default style
                if (style.write().AddAttribute(*a, this))
                    MyCurrentContext().styles[s] = style;
                else {
                    problem.push_back(s);
                    had_generic |= (style.read().type == EStyleType::STYLE);
                }
            }
            if (problem.size()==0) continue;
            string msg;
            if (problem.size()==1) {
                if (had_generic)
                    msg = "Attribute '" + a->name + "' is not applicable to styles. Ignoring it.";
                else
                    msg = "Attribute '" + a->name + "' is not applicable to style '" + *problem.begin() + "'. Ignoring it.";
            } else if (problem.size() == sl.size()) {
                if (had_generic)
                    msg = "Attribute '" + a->name + "' is not applicable to styles. Ignoring it.";
                else
                    msg = "Attribute '" + a->name + "' is not applicable to any of these styles. Ignoring it.";
            } else {
                msg = "Attribute '" + a->name + "' is not applicable to styles '" + *problem.begin();
                for (auto p = ++problem.begin(); p!=--problem.end(); p++)
                    msg.append("', '").append(*p);
                msg.append("' and '").append(*--problem.end()).append("'. Ignoring it.");
                _ASSERT(!had_generic);
            }
            Error.Error(*a, false, msg);
        }
    else //empty styles may be defined
        for (auto &s : sl)
            MyCurrentContext().styles[s] = MyCurrentContext().styles.GetStyle(s); //may be the default style
}

/** Draw the page breaks */
template <class Ctx, class Prog, class RefData, class PageData>
void ChartBase<Ctx, Prog, RefData, PageData>::DrawPageBreaks(Canvas &canvas)
{
    //single page
    if (pageBreakData.size()<=1) return;
    //nothing to draw
    if (total.y.Spans()<=0) return;
    //last page is degenerate - otherwise single page
    _ASSERT(pageBreakData.back().wh.x && pageBreakData.back().wh.y);
    if (pageBreakData.back().wh.x == 0 || pageBreakData.back().wh.y == 0)
        if (pageBreakData.size()==2)
            return;
    LineAttr line;
    StringFormat format;
    format.Default();
    format.Apply("\\pr\\-");
    Label label;
    double last_y = DBL_MIN;
    for (unsigned page = 1; page<pageBreakData.size(); page++) {
        //if this is the last page and it is of zero size, we skip drawing a page end for it.
        if (page == pageBreakData.size()-1 &&
              (pageBreakData[page].wh.x==0 || pageBreakData[page].wh.y==0))
            break;
        char text[20];
        const double y = pageBreakData[page].xy.y;
        line.type = pageBreakData[page].manual ? ELineType::DASHED : ELineType::DOTTED;
        if (canvas.does_graphics())
            canvas.Line(XY(total.x.from, y), XY(total.x.till, y), line);
        else
            canvas.Add(GSPath({ XY(total.x.from, y), XY(total.x.till, y) }, line));
        snprintf(text, sizeof(text), "page %u", page);
        label.Set(text, canvas, Shapes, format);
        const double ylabelpos = y-label.getTextWidthHeight().y;
        if (last_y<ylabelpos)
            //dont draw page number for very short (likely zero-height) pages
            label.Draw(canvas, Shapes, total.x.from, total.x.till, ylabelpos);
        last_y = y;
    }
}



/** Override this to draw the page header and footer (and lefting and righting).
 * The default implementation of ChartBase:: just draws the copyright text.*/
template <class Ctx, class Prog, class RefData, class PageData>
void ChartBase<Ctx, Prog, RefData, PageData>::DrawHeaderFooter(Canvas &canvas, unsigned page)
{
    _ASSERT(page<=pageBreakData.size());
    canvas.PrepareForHeaderFooter();

    if (copyrightTextHeight==0 || copyrightText.empty()) return; //Dont do nothing if there is no copyright text.

    const auto [xr, y] = [this, page] {
        if (page==0) return std::pair(total.x, total.y.till);
        return std::pair(Range(pageBreakData[page-1].xy.x, pageBreakData[page-1].xy.x + pageBreakData[page-1].wh.x),
                         pageBreakData[page-1].xy.y + pageBreakData[page-1].wh.y);
    }();

    //Draw background for copyright text. Calculate best fill.
    //-1 here makes the fill connect to the chart background without any gap. (Hack I know.)
    const Block B(xr, { y-1, y+copyrightTextHeight });
    const FillAttr f = GetBkFill({ y, y+copyrightTextHeight }, false);
    if (canvas.does_graphics())
        canvas.Fill(B, f);
    else
        canvas.Add(GSBox(B, LineAttr::None(), f));

    //Draw Copyright Text
    StringFormat sf; sf.Default();
    Label(copyrightText, canvas, Shapes, sf).Draw(canvas, Shapes, xr.from, xr.till, y);
}

//Deletes everything, we will contain NO pages - descendants must add an empty page
template<class Ctx, class Prog, class RefData, class PageData>
inline void ChartBase<Ctx, Prog, RefData, PageData>::SetToEmpty()
{
    Progress.Reset();
    Contexts.clear();
    Designs.clear();
    ReferenceNames.clear();
    pageBreakData.clear();
    Chart::SetToEmpty();
}

/** The state parsing may reach an opening '{' brace.*/
enum class EScopeOpenMode
{
    NORMAL,      ///<Nothing special (Used also by procedure definitions. There the fact that we parse a procedure can be captured by bison rules.)
    PROC_REPLAY  ///<A procedure invocation preceded the brace, which is then from the text of the proc body
};

/* For languages supporting procedures and replay,
 * this structure is used during parsing between the
 * header of the procedure and its opening '{'. */
struct procedure_parse_helper {
    ParameterContext last_procedure_params;    ///<Place parameter list of a procedure definition header here to be available to the body
    const Procedure* last_procedure = nullptr; ///<After parsing a procedure header put the looked-up procedure here.
    EScopeOpenMode   open_context_mode = EScopeOpenMode::NORMAL; ///<What was there before a scope opening?
};

#endif