#pragma once
#include <string_view>
#include <fstream>
#include <ranges>
#include <filesystem>
#include <random>
#include <variant>
#include "version.h"
#include "error.h"
#include "cgen_attribute.h"
#include "stringparse.h"


struct EmbedChartData {
    constexpr static std::string_view ALT_TEXT_PREAMBLE{ "Msc-generator~|" };
    std::string chart_type;  ///<The primary extension of the chart type (block, signalling, graph)
    std::string chart_text;  ///<The source text of the chart (unescaped)
    std::array<int, 3> version = { LIBMSCGEN_MAJOR, LIBMSCGEN_MINOR, LIBMSCGEN_SUPERMINOR };            ///<The version of Msc-generator used to produce this chart.
    XY chart_size{ 0.,0. };  ///<The unscaled total size of the chart (in points (=in chart space), including header/footer)
    std::string gui_state = {};   ///<The serialized (binary) GUI state for entity collapse/expand
    unsigned page = 0;
    static EmbedChartData ParseEscapedText(std::string_view);
    std::string Escape() const;
    bool empty() const noexcept { return chart_type.empty(); }
    void clear() noexcept { operator =({}); }
};

struct GenericShape;
struct SimpleTransform;
enum class EPageSize;

std::string my_tmpnam();

struct FileInZip {
    std::string inner_filename;
    std::string content;
};

class ZipArchive : std::variant<const std::string*, std::string*> {//index==0: UTF-8 filename, index==1: zip file content in memory
    using std::variant<const std::string*, std::string*>::variant;
public:
    constexpr bool is_file() const noexcept { return this->index()==0; }
    const std::string& fname() const { return *std::get<0>(*this); }
    std::string qfname_or_clipboard() { return is_file() ? "'"+fname()+"'" : "the Clipboard"; }
    std::string& mem() const { return *std::get<1>(*this); }
    static ZipArchive FromFile(const std::string& utf8_filename) { return ZipArchive(std::in_place_index<0>, &utf8_filename); }
    static ZipArchive FromMem(std::string& compressed_content) { return ZipArchive(std::in_place_index<1>, &compressed_content); }
};

/** Reads the list of files in the archive. The names (with full path) will be added
 * to 'files' bit the content will be empty. Returns the human-readable string
 * on error or empty if none. Defined in zips.cpp. */
std::string GetFilesInZip(ZipArchive archive, std::vector<FileInZip>& files);
/** Reads the content of certain files in the archive. The files to read shall be in 'files'
 * and after the call the content will be filled. Files that dont exist in the archive will
 * result in an error. Returns the human-readable string on error or empty if none.
 * Defined in zips.cpp. */
std::string ExtractFromZip(ZipArchive archive, std::vector<FileInZip>& files);
/** Adds to a zip file already existing on disk or in memory. Returns the human-readable string
 * on error or empty if none. Defined in zips.cpp. If the files to add are already in the
 * archive, we replace them to this new content. If we return an error, the original archive
 * is kept unchanged.*/
std::string AddToZip(ZipArchive archive, const std::vector<FileInZip>& files);

/** A PPT file. This object may or may not be associated with a file.
 * If not associated with a file, new_file() can be called.
 * If associated with a file, add() can be called to add objects and flush() can be called to
 * write the added objects to the file.
 * del_file() can be called to remove the file and disassociate the object from the file.
 * On destruction any file associated will NOT be removed.
 * The last error is stored in the object irrespective of file association status.*/
class PptFile {
    std::string file_name;  ///<Set to nonempty if we have successfully written the default file out.
    std::string content;    ///<The accumulated content of the 'ppt/slides/slide1.xml' file
    std::string err;        ///<Set to human readable error string on an error
    std::string rel_content;///<The accumulated content of the 'ppt/slides/_rels/slide1.xml.rels' file
    int shape_id = 1;// p:cNvPr is #1
    std::vector<std::string> links; ///<List of hyperlinks, first one is id=2.
    std::vector<SimpleTransform> transform; //apply these when adding any shape.
    Block clip;             ///<We reject any shapes outside this
    Block clip_hdr;         ///<The area that includes the header, lefting and the copyright text. Copied to 'clip' in prepare_header_footer().
    XY page_size_in_points;
    unsigned page = 1;  //number of the current page (incremented on flush_page)
    unsigned GetLinkId(std::string_view url);
    std::string MakeLabel(const Label& L, std::string_view asterisk_replacement, double scale_font);
    std::string MakeElement(const Block& rect, const LineAttr&, ArrowHead const* start, ArrowHead const* end,
                            std::string outer, std::string header, std::string header2,
                            std::string properties = {}, std::string extra = {}, std::string transforms = {});
    void reset();

public:
    /** Creates the template pptx file on disk of a given number of pages and of a specific page size.
     *  On error we remove whatever we created (if we can) and set status to error.
     * @returns false if the object already contained a file (no error set) or on error (error set)*/
    bool new_file(std::string_view fname, int pageno, const XY& page_size_in_points);
    /** Set the transformation applied to elements added to us. The elements will be added to us
     * in the chart space coordinates. This function calculates how to map elements of a given
     * page to the current PPT page.
     * @param [in] origSize The size of the page in chart units.
     * @param [in] origOffset The top-left coordinate of the page in chart units.
     * @param [in] headingLeftingSize How much space to spare at the top and left side
     * @param [in] user_scale The scaling factor calculated from chart size, heading size
     *                        and page size elsewhere.
     * @param [in] h_alignment Contains -1/0/+1 for left/center/right page alignment.
     * @param [in] v_alignment Contains -1/0/+1 for up/center/down page alignment.
     * @param [in] raw_page_clip The net page size in points, taking margins into account or (0,0,0,0) if no margin
     * @param [in] copyrightTextHeight The height of the copyright text
     * @returns false on error (invalid or too small size or no file) and then also
     * sets the object to the error state.*/
    bool set_page_size(const XY &origSize, const XY &origOffset,
                       const XY &headingLeftingSize, const XY &user_scale,
                       int h_alignment, int v_alignment, const Block &raw_page_clip,
                       double copyrightTextHeight);
    /** Call this when done drawing the chart and before drawing the header, lefting
     * and footer (copyright text).*/
    void prepare_header_footer() { clip = clip_hdr; }
    /** Resets the object (deleting any file associated) */
    void del_file();
    /** Appends the content to the pptx as a new page.
     * On success, we increase page count, reset content to the preamble which allows adding new
     * objects, which will land on the next page. You can also stop writing to this file, it is
     * valid as it is.
     * @param [in] force_empty If set and nothing was added, we emit an empty page, else nothing.
     * @returns false on error & also removes the pptx from disk. You can then read the error.
     *          We also return false (with no error set) if the object is associated with no file.*/
    bool flush_page(bool force_empty = true);
    /** Finalizes the file, writing out page count, etc.
     * We return true on success (and the PptFile object is reseted.)
     * On a not opened file, we return false (and the PptFile object remains reseted.
     * On a pre-existing error, we return false (and the error remains)
     * On a problem in finalizing the file, we set the error (and delete the file)*/
    bool finalize();
    /** Adds a shape, currently to slide1.
     * @returns false if no file (no error set) or on err (and also sets the ppt slide to err)*/
    bool add(GenericShape&& shape);
    /** Adds shapes, currently to slide1.
     * @returns false if we are already in error (in which case we do nothing) or
     * true if we succeeded. We never result in an error by ourselves. */
    template <typename container> requires(requires(container C) { std::begin(C); std::end(C); })
    bool add(container &&shapes) {
        for (auto &shape : shapes)
            if (!add(std::move(shape))) return false;
        return true;
    }
    /** Returns the description of the current error state or empty if OK.*/
    std::string const& error() const noexcept { return err; }
    /** Returns true if we have a file and not in the error state. */
    bool is_ok() const noexcept { return err.empty() && file_name.size(); } //true if valid and we have a file set

private:
    struct Slide {
        unsigned id;          //The id in ppt/_rels/presentation.xml.rels, like 257
        std::string rId;      //The ref Id in ppt/_rels/presentation.xml.rels, like rId2
        std::string filename; //The filename in slides (includes partial path under 'ppt', like 'slides/slide1.xml')
    };
    /** Parses a PPT and extracts the xml filenames of the pages in order.
     * Usually these are "slide1.xml", "slide2.xml", but you may never know.
     * We can also return the slide size in EMUs, if needed.*/
    static std::string parse_pages(ZipArchive archive, std::vector<Slide>& slides, std::pair<int, int>* slide_size=nullptr);

    /** Append an empty page to the PPT presentation.
     * @returns the page number (starting from 1) and an error string (or empty)*/
    static std::string append_page(ZipArchive archive, unsigned *page = nullptr);

    constexpr static const char chart_name_template[] = "Msc-generator Chart %llu.%s"; //Random number plus chart type

public:
    struct ChartToPPT {
        EmbedChartData data;
        mutable unsigned slide, chart;
        std::string png_fn;  //PNG file to embed (on disk, name in UTF-8)
    };
    /** Adds an image to a given page of the presentation.
     * If the page is zero, we append a new page first.
     * @param [in] archive The filename of the PPT (it shall be closed) or its content loaded.
     * @param [in] png_fn The disk filename of the PNG image to add. Whithin the PPT a new,
     *                    random name is generated.
     * @param [in] page The page number to add to (starting from 1) If zero, we append a new page.
     * @param [in] chart The index of the chart within the page to replace (from 1). If zero, we add a
     *                   new chart to the slide (centered) and return the resulting index here.
     *                   If positive and there is less charts on the page, we return an error.
     * @param [in] chart_data Type, chart text, version and chart_size -> everything encoded in Alt Text
     * @param [in] chart_text The chart text to embed.
     * @param [in] chart_size The chart size in chart space.
     * @param [in] OpenNamedFile function to open a file by an UTF-8 name*/
    static std::string add_image(ZipArchive archive, const std::string& png_fn, unsigned& slide, unsigned& chart,
                                 EmbedChartData &&data, OpenNamedFileFunction OpenNamedFile) {
        std::vector<ChartToPPT> charts{ { std::move(data), slide, chart, png_fn } };
        std::string err = add_images(archive, charts, OpenNamedFile);
        slide = charts.front().slide;
        chart = charts.front().chart;
        return err;
    }

    /** Adds several charts to the PPT in one go. The operation is atomic, it either succeeds completetly or fails,
     * with the exception of adding empty pages at the end. These happen first and after them, the op may still fail.*/
    static std::string add_images(ZipArchive archive, const std::vector<ChartToPPT>& charts, OpenNamedFileFunction OpenNamedFile);

    struct ChartInPPT {
        EmbedChartData data;
        unsigned slide, chart;     //Which page the image is on (starts from 1) and what is its number (starts from one)
        std::string internal_name; //The internal filename in /media (not including any path component)
        std::string rId;           //The ref on the page used (e.g., "rId3")
        unsigned id;               //The id within the page (value of id attribute)
        int x, y;                  //The position of the chart on the slide in EMUs
        unsigned w, h;             //The dimensions of the chart on the page in EMUs (after cropping)
        int crop_l, crop_r;        //The left/right crop in percentage of width times 1000. Positive is crop, negative is outset.
        int crop_t, crop_b;        //The top/bottom crop in percentage of height times 1000. Positive is crop, negative is outset.
        /// Rectangle size visible due to crop in chart space units.
        XY crop(const XY &chart_size) const noexcept {
            return {chart_size.x * (100. - crop_l / 1000. - crop_r / 1000.),
                    chart_size.y * (100. - crop_t / 1000. - crop_b / 1000.)};
        }
    };
    /** Extracts the Msc-generator charts present in the PPT.
     * The returned values are not sanitized. That is version, chart type and internal_filename
     * are returned as-is and the caller should emit error messages.*/
    static std::string parse_charts(ZipArchive archive, std::vector<ChartInPPT>& charts, std::vector<std::string>* pages = nullptr);
};

/** Contains an Art::GVML ClipFormat clipboard item, parsed*/
class PptClipboard {
    std::string zipped_content; //the chart zipped in Art::GVML ClipFormat
    std::vector<PptFile::ChartInPPT> charts;
    std::string error; //Used only while debugging
public:
    PptClipboard() noexcept = default;
    explicit PptClipboard(std::string&& zipped_content);            ///<Parses the clipboard content for Msc-gen embedded charts: the zipped Art::GVML ClipFormat
    PptClipboard(PptClipboard&&) noexcept = default;
    PptClipboard(const PptClipboard&) noexcept = default;
    PptClipboard& operator=(PptClipboard&&) noexcept = default;
    PptClipboard& operator=(const PptClipboard&) noexcept = default;

    size_t size() const noexcept { return charts.size(); }     ///<The number of charts found. May be 0.
    bool empty() const noexcept { return charts.empty(); }
    void clear() noexcept { zipped_content.clear(); charts.clear(); error.clear(); }
    const std::vector<PptFile::ChartInPPT> &GetCharts() const noexcept { return charts; }
    const PptFile::ChartInPPT* FindChart(unsigned slide, unsigned chart) const noexcept {
        const auto i = std::ranges::find_if(charts, [slide, chart](const PptFile::ChartInPPT& c) { return slide == c.slide && chart == c.chart; });
        return i == charts.end() ? nullptr : &*i;
    }
    std::string update(const std::vector<PptFile::ChartToPPT>& C,
                       OpenNamedFileFunction OpenNamedFile);        ///<Updates the charts. Returns any error (and leaves the state unchanged). May be called several times.
    const std::string& GetZippedContent() const noexcept { return zipped_content; } ///<Returns the current zipped content

    static std::string create(EmbedChartData &&data, std::string&& png_content);  //creates a fresh, zipped Art::GVML ClipFormat object to be put to the clipboard.
};

/** Describes what we may have found in the clipboard.
 * - monostate: nothing
 * - PptClipboard: A fully formed Art::GVML Clipformat object, which
 *   also contains the context of the chart (e.g., size, border, etc.)
     and may also contain other graphics elements - even multiple charts.
 * - EmbedChartData: Escaped text, from which we have decoded a chart. */
using ClipboardSeen = std::variant<std::monostate, PptClipboard, EmbedChartData>;
