/*
    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_shapes.cpp The declaration of Shapes.
 * @ingroup libcgencommon_files  */

#define _CRT_SECURE_NO_DEPRECATE //For visual studio to allow sscanf
#include "cgen_shapes.h"

constexpr const char ShapeElement::act_code[];

string ShapeElement::ErrorMsg(Type t, int numargs)
{
    string ret;
    if (GetNumArgs(t)==numargs) return ret;
    switch (t) {
    default: _ASSERT(0); ret = "Internal error."; return ret;
    case LINE_TO: ret = "L (Line_to) commands require two numbers, an X and an Y coordinate."; break;
    case MOVE_TO: ret = "M (Move_to) commands require two numbers, an X and an Y coordinate."; break;
    case CURVE_TO: ret = "C (Curve_to) commands require six numbers, three 2D points: endpoint and two control points."; break;
    case CLOSE_PATH: ret = "E (close path) commands require no parameters."; break;
    case HINT_AREA: ret = "H (Hint area) commands require four numbers, two 2D points to represent a rectangle."; break;
    case TEXT_AREA: ret = "T (Text area) commands require four numbers, two 2D points to represent a rectangle."; break;
    case SECTION_BG:
    case SECTION_FG_LINE:
    case SECTION_FG_FILL:
    case SECTION_FG_BG: ret = "S (section) commands require one number: the section number [0-3]"; break;
    case PORT: ret = "P (Port) commands require two numbers, an X and an Y coordinate and the name of the port (case sensitive and alphanumeric) and optionally its direction (0-360 degrees from north clockwise; -1: no specific dir; -2:perpendicular."; break;
    }
    return ret;
}


string ShapeElement::Write() const
{
    string ret(1, act_code[action]);
    if (SECTION_FG_BG >= unsigned(action) && unsigned(action) >= SECTION_BG)
        ret << " " << unsigned(action) - unsigned(SECTION_BG);
    else if (action == TEXT_AREA || action == HINT_AREA || action==CURVE_TO)
        ret << " " << x1 << " " << y1 << " " << x2 << " " << y2;
    else if (action == PORT)
        ret << " " << str << x << " " << y;
    else if (action != CLOSE_PATH) 
        ret << ' ' << x << " " << y;
    ret.append("\n");
    return ret;
}


/** Helper to map a position (x) in a range (source) to the corresponding
 * position in another range (target).*/
constexpr double Tr(double x, const Range &source, const Range &target) noexcept
{
    return (x-source.from)*(target.Spans()/source.Spans()) + target.from;
}

Shape::Shape(std::string_view n, const FileLineCol &l,
             std::string_view u, std::string_view i) :
    cover_fresh(true),
    current_section(1),
    name(n),
    definition_pos(l),
    url(u),
    info(i)
{
    max.MakeInvalid();
    label_pos.MakeInvalid();
    hint_pos.MakeInvalid();
}

Shape::Shape(std::string_view n, const FileLineCol &l,
             std::string_view u, std::string_view i,
    Shape &&s) :
    max(s.max),
    label_pos(s.label_pos),
    hint_pos(s.hint_pos),
    ports(std::move(s.ports)),
    bg(std::move(s.bg)),
    fg_fill(std::move(s.fg_fill)),
    fg_line(std::move(s.fg_line)),
    cover_fresh(s.cover_fresh),
    cover(std::move(s.cover)),
    current_section(s.current_section),
    name(n),
    definition_pos(l),
    url(u),
    info(i)
{
}

const Contour &Shape::GetCover() const
{
    if (!cover_fresh) {
        Contour b(bg, ECloseType::CLOSE_ALL_CONNECTED, EForceClockwise::INVERT_IF_NEEDED, true);
        Contour f1(fg_fill, ECloseType::CLOSE_ALL_CONNECTED, EForceClockwise::INVERT_IF_NEEDED, true);
        Contour f2(fg_line, ECloseType::CLOSE_ALL_CONNECTED, EForceClockwise::INVERT_IF_NEEDED, true);
        cover = std::move(b) + std::move(f1) + std::move(f2);
        cover_fresh = true;
    }
    return cover;
}

/** Take the continuous path in current_section_data and append it to the current section.
 * Empty current_section_data and invalidate cover.*/
void Shape::ProcessSubPath(bool close)
{
    if (current_section_data.empty()) return;
    if (close && current_section_data.front().GetStart() != current_section_data.back().GetEnd())
        current_section_data.emplace_back(current_section_point, XY(current_section_data.front().GetStart()));
    if (current_section == 3 && current_section_data.IsClosed() && current_section_data.IsConnected()) 
        bg.append(current_section_data);
    GetSection(current_section).append(std::move(current_section_data));
    cover_fresh = false;
    current_section_data.clear();
}

void Shape::Add(ShapeElement &&e)
{
    switch (e.action) {
    case ShapeElement::PORT:
        if (e.str.length())
            ports.insert({std::move(e.str), Port{XY(e.x, e.y), e.x1}}); //this is a no-op if port already added
        break;
    case ShapeElement::HINT_AREA:
        hint_pos = Block(e.x, e.x1, e.y, e.y1);
        break;
    case ShapeElement::TEXT_AREA:
        label_pos = Block(e.x, e.x1, e.y, e.y1);
        max += label_pos;
        break;
    default:
        _ASSERT(0);
        break;
    case ShapeElement::CURVE_TO:
        current_section_data.emplace_back(current_section_point, XY(e.x, e.y),
            XY(e.x1, e.y1), XY(e.x2, e.y2));
        max += current_section_data.back().CreateBoundingBox();
        current_section_point = XY(e.x, e.y);
        break;
    case ShapeElement::LINE_TO:
        current_section_data.emplace_back(current_section_point, XY(e.x, e.y));
        max += XY(e.x, e.y);
        current_section_point = XY(e.x, e.y);
        break;
    case ShapeElement::MOVE_TO:
        ProcessSubPath(false); //If we already have data, this is an open segment, don't close
        max += XY(e.x, e.y);
        current_section_point = XY(e.x, e.y);
        break;
    case ShapeElement::CLOSE_PATH:
        //Add closing edge, if needed
        ProcessSubPath(true); 
        break; 
    case ShapeElement::SECTION_BG:
        ProcessSubPath(current_section<2); //make previous section 0 and 1 closed
        current_section = 0;
        break;
    case ShapeElement::SECTION_FG_FILL:
        ProcessSubPath(current_section<2); //make previous section 0 and 1 closed
        current_section = 1;
        break;
    case ShapeElement::SECTION_FG_LINE:
        ProcessSubPath(current_section<2); //make previous section 0 and 1 closed
        current_section = 2;
        break;
    case ShapeElement::SECTION_FG_BG:
        ProcessSubPath(current_section < 2); //make previous section 0 and 1 closed
        current_section = 3;
        break;
    }
}

/** Get the position of the label inside this shape, if the shape 
 * is drawn in the bounding box 'o'. Returns an invalid box if the 
 * shape has no label position*/
Block Shape::GetLabelPos(const Block &o) const noexcept
{
    if (label_pos.IsInvalid()) return label_pos;
    return Block(Tr(label_pos.x.from, max.x, o.x), Tr(label_pos.x.till, max.x, o.x),
        Tr(label_pos.y.from, max.y, o.y), Tr(label_pos.y.till, max.y, o.y));
}

/** Returns the coordinates of a port if the shape is  drawn into box 'o'.
 * If the port name is not found, we don't return a value.*/
std::optional<Shape::Port> Shape::GetPort(const std::string &port, const Block & o) const
{
    auto i = ports.find(port);
    if (i==ports.end()) return {};
    return Port{XY(Tr(i->second.xy.x, max.x, o.x), Tr(i->second.xy.y, max.y, o.y)), i->second.dir};
}

std::optional<Shape::Port> Shape::GetPort(size_t port, const Block & o) const
{
    if (ports.size()<=port) return {};
    auto i = ports.begin();
    while (port) {
        port--;
        i++;
    }
    return Port{XY(Tr(i->second.xy.x, max.x, o.x), Tr(i->second.xy.y, max.y, o.y)), i->second.dir};
}

/** Return the path of the given section transformed as if the shape is drawn into 
 * bounding box 'o'.*/
Path Shape::GetSection(unsigned section, const Block & o) const
{
    _ASSERT(section<=2);
    if (section>2)
        return {};
    if (o.x.Spans()<=0 || o.y.Spans()<0)
        return {};
    Path p = GetSection(section).CreateScaled({o.x.Spans()/max.x.Spans(), o.y.Spans()/max.y.Spans()});
    p.Shift({o.x.from - max.x.from*o.x.Spans()/max.x.Spans(), o.y.from - max.y.from*o.y.Spans()/max.y.Spans()});
    return p;
}


/** Puts the shape's path (the given section) to the cairo context,
 * transformed such that the bounding box of the shape is give by 'o'.*/
void Shape::CairoPath(cairo_t *cr, unsigned section, const Block &o) const
{
    Path p = GetSection(section, o);
    if (section<2)
        Contour(std::move(p), ECloseType::CLOSE_ALL_CONNECTED).CairoPath(cr, false);
    else
        p.CairoPath(cr, false);
}

/** Draws a shape to a canvas.
 * @param canvas The canvas to draw onto
 * @param o The outer bounding box to fit the shape into.
 * @param line Use this style for sections 1 and 2.
 * @param fill Use this style for section 0.
 * @param shadow This is used to add shadow to section 0.*/
void Shape::Draw(Canvas & canvas, const Block & o, const LineAttr & line, const 
                 FillAttr & fill, const ShadowAttr &shadow) const
{
    cairo_t * const cr = canvas.GetContext();
    if (!cr) return;
    if (GetSection(0).size() && shadow.color && shadow.offset.value_or(0))
        canvas.Shadow(Contour(GetSection(0, o), ECloseType::CLOSE_ALL_CONNECTED), shadow);
    if (GetSection(0).size() && fill.color && !fill.color->IsFullyTransparent()) {
        CairoPath(cr, 0, o);
        cairo_save(cr);
        cairo_clip(cr);
        canvas.Fill(o, fill);
        cairo_restore(cr);
    }
    if (line.type && *line.type!=ELineType::NONE && line.color && !line.color->IsFullyTransparent()) {
        if (GetSection(1).size()) {
            FillAttr linefill = FillAttr::Solid(*line.color);
            CairoPath(cr, 1, o);
            cairo_save(cr);
            cairo_clip(cr);
            canvas.Fill(o, linefill);
            cairo_restore(cr);
        }
        if (GetSection(2).size() && line.width && *line.width>0) {
            CairoPath(cr, 2, o);
            canvas.SetLineAttr(line);
            cairo_stroke(cr);
        }
    }
}

ShapeCollection & ShapeCollection::operator = (const ShapeCollection &o)
{
    if (&o==this) return *this;
    shapes.clear();
    shapes.reserve(o.shapes.size());
    for (auto s : o.shapes)
        shapes.emplace_back(s);
    return *this;
}


/** Parses a shape description and adds it.
Syntax:
N name: starts a new Shape
S section: starts a section. 0=background of the Shape (shall be closed),
1=foreground of the Shape (will be filled, shall be closed),
2=foreground of the Shape (will be stroked, can be open)
M x y: MoveTo operation within a section
L x y: Lineto operation within a section
C x y x y x y: CurveTo, with the first coordinate being the endpoint
the other two being the two control points.
E: Closes a path
T x y x y: Specifies a box where a label can be inserted. If omitted label
will be placed below the Shape.
H x y x y: Specifies the portion, which shall be shown for hints. If omitted
all of the Shape will be shown.
*/

bool ShapeCollection::Add(std::string_view shape_text, MscError &Error)
{
    const size_t old_size = shapes.size();
    string url, info;
    FileLineCol url_pos;
    FileLineCol fpos(int(Error.Files.size()-1), 1, 1);
    size_t pos = 0;
    bool block = false;
    double a, b, c, d, e, f;
    while (shape_text[pos]) {
        const size_t line_begin = pos;
        while (shape_text[pos]==' ' || shape_text[pos]=='\t') pos++;
        fpos.col = pos-line_begin+1;
        size_t end_pos = pos;
        while (shape_text[end_pos] && shape_text[end_pos]!='\n') end_pos++;
        switch (shape_text[pos]) {
        case 'U':
            if (old_size!=shapes.size())
                Error.Warning(fpos, "URL information should come at the beginning of the file.");
            if (url.length()) {
                Error.Error(fpos, "Only one URL can be specified per file. Ignoring this URL.");
                Error.Error(url_pos, fpos, "Location of previous URL.");
            } else {
                url.assign(shape_text.data() + pos + 2, end_pos - 2 - pos);
                url_pos = fpos;
            }
            break;
        case 'I':
            if (old_size!=shapes.size())
                Error.Warning(fpos, "Description info ('I') should come at the beginning of the file.");
            info.append(shape_text.data() + pos + 2, end_pos - 1 - pos); //attach trailing zero or \n
            info.back() = '\n'; //force \n at end
            break;
        case 'N':
            pos++;
            while (shape_text[pos]==' ' || shape_text[pos]=='\t') pos++;
            {
                string name(shape_text.data()+pos, end_pos-pos);
                if (name.length())
                    while (name.back()==' ' || name.back() == '\t')
                    name.erase(name.length()-1);
                if (name.length()==0) {
                    fpos.col = pos-line_begin+1;
                    Error.Error(fpos, "No name specified. Skipping this Shape.");
                    block = true;
                    break;
                }
                if (GetShapeNo(name)>=0) {
                    fpos.col = pos-line_begin+1;
                    Error.Error(fpos, "Shape with name '" + name + "'is already specified. Skipping this Shape.");
                    Error.Error(shapes[GetShapeNo(name)].definition_pos, fpos, "Location of the previous definition.");
                    block = true;
                    break;
                }
                if (shapes.size()>0 && shapes.back().IsEmpty()) {
                    Error.Error(shapes.back().definition_pos,
                        "Shape '" + shapes.back().name + "' contains no actual Shape. Ignoring it.");
                    shapes.resize(shapes.size()-1);
                }
                shapes.emplace_back(name, fpos, url, info);
                block = false;
            }
            break;
        case 'L':
        case 'M':
            if (block) break;
            if (sscanf(shape_text.data()+pos+1, "%lf %lf", &a, &b) == 2)
                shapes.back().Add(ShapeElement(shape_text[pos]=='M' ? ShapeElement::MOVE_TO : ShapeElement::LINE_TO, a, b, 0, 0, 0, 0));
            else
                Error.Error(fpos, "Could not read two numbers separated by spaces. Ignoring line.");
            break;
        case 'C':
            if (block) break;
            if (sscanf(shape_text.data()+pos+1, "%lf %lf %lf %lf %lf %lf", &a, &b, &c, &d, &e, &f) == 6)
                shapes.back().Add(ShapeElement(ShapeElement::CURVE_TO, a, b, c, d, e, f));
            else
                Error.Error(fpos, "Could not read six numbers separated by spaces for a 'C'urve_to operation. Ignoring line.");
            break;
        case 'E':
            if (block) break;
            shapes.back().Add(ShapeElement(ShapeElement::CLOSE_PATH, 0, 0, 0, 0, 0, 0));
            break;
        case 'S':
            if (block) break;
            if (shape_text[pos+1]==' ' && unsigned(shape_text[pos+2]-'0') <=3)
                switch (shape_text[pos+2]) {
                case '0': shapes.back().Add(ShapeElement(ShapeElement::SECTION_BG, 0, 0, 0, 0, 0, 0)); break;
                case '1': shapes.back().Add(ShapeElement(ShapeElement::SECTION_FG_FILL, 0, 0, 0, 0, 0, 0)); break;
                case '2': shapes.back().Add(ShapeElement(ShapeElement::SECTION_FG_LINE, 0, 0, 0, 0, 0, 0)); break;
                case '3': shapes.back().Add(ShapeElement(ShapeElement::SECTION_FG_BG, 0, 0, 0, 0, 0, 0)); break;
                } else
                Error.Error(fpos, "'S' should be followed by a space and an integer between 0 and 3. Ignoring this line.");
                break;
        case 'T':
            if (block) break;
            if (sscanf(shape_text.data()+pos+1, "%lf %lf %lf %lf", &a, &b, &c, &d) == 4) {
                shapes.back().Add(ShapeElement(ShapeElement::TEXT_AREA, a, b, c, d, 0, 0));
            } else
                Error.Error(fpos, "Could not read four numbers separated by spaces for a 'T'ext_box spec. Ignoring line.");
            break;
        case 'H':
            if (block) break;
            if (sscanf(shape_text.data()+pos+1, "%lf %lf %lf %lf", &a, &b, &c, &d) == 4)
                shapes.back().Add(ShapeElement(ShapeElement::HINT_AREA, a, b, c, d, 0, 0));
            else
                Error.Error(fpos, "Could not read four numbers separated by spaces for a 'H'int_box spec. Ignoring line.");
            break;
        default:
            if (end_pos == pos) break; //empty line
            if (shape_text[pos]=='#') break;
            if (shape_text[pos]=='/' && shape_text[pos+1]=='/') break;
            Error.Error(fpos, "Lines must start with one of the letters 'UINTHMLCES' or '#' for comments. Ignoring this line.");
        }
        if (shape_text[end_pos] == 0) break;
        pos = end_pos+1;
        fpos.line++;
    }
    if (shapes.size()>0 && shapes.back().IsEmpty()) {
        Error.Error(shapes.back().definition_pos,
            "Shape '" + shapes.back().name + "' has no actual content. Ignoring it.");
        shapes.resize(shapes.size()-1);
    }
    return Error.hasErrors();
}

/** Adds a parsed shape to the collection.
 * @param name The name of the shape
 * @param fpos The place where it was defined.
 * @param u The URL of the file parsed from
 * @param i Some human readable info on the shape file.
 * @param s The shape to add (this is a move, '*s' will become invalid)
 * @param error Where to report an error, e.g., on name duplication.
 * @return true on success.*/
bool ShapeCollection::Add(std::string_view name, const FileLineCol &fpos,
                          std::string_view u, std::string_view i,
                          Shape &&s, MscError &error)
{
    if (GetShapeNo(name)>0) {
        error.Error(fpos, "Shape with name '" + std::string(name) + "'is already specified. Skipping this Shape.");
        error.Error(shapes[GetShapeNo(name)].definition_pos, fpos, "Location of the previous definition.");
        return true;
    } else {
        shapes.emplace_back(name, fpos, u, i, std::move(s));
        return false;
    }
}


void ShapeCollection::AttributeValues(Csh &csh) const
{
    for (unsigned u = 0; u<shapes.size(); u++)
        csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRVALUE)+shapes[u].name,
            nullptr, EHintType::ATTR_VALUE, true,
            CshHintGraphicCallbackForEShapes,
            CshHintGraphicParam(u)));
}

std::vector<const string*> ShapeCollection::ShapeNames() const
{
    std::vector<const string*> ret(shapes.size());
    for (unsigned u = 0; u<shapes.size(); u++)
        ret[u] = &shapes[u].name;
    return ret;
}

/**Return the list of shapes, but collapse ones that are similar and too different
* from the (nonexistent) name specified by the user. */
std::vector<string> ShapeCollection::ShapeNames(std::string_view partial) const
{
    //Copy all shape
    std::vector<string> ret;
    std::vector<const string*> tmp = ShapeNames();
    if (tmp.size()<7) { //in case of a few elements only, use them verbatim
        for (auto &n : tmp)
            ret.push_back(*n);
        return ret;
    }
    std::sort(tmp.begin(), tmp.end(), [](const string *p1, const string *p2) {return *p1<*p2; });
    for (auto &n : tmp) {
        unsigned len_user = CaseInsensitiveCommonPrefixLen(partial, n->c_str());
        while (len_user && (*n)[len_user-1]!='.')
            len_user--;
        unsigned len_prev = ret.size() ? CaseInsensitiveCommonPrefixLen(ret.back().c_str(), n->c_str()) : 0;
        while (len_prev && (*n)[len_prev-1]!='.')
            len_prev--;
        if (ret.size()==0 || len_user>=len_prev)
            ret.push_back(*n);
        else {
            //merge with previous
            ret.back().erase(len_prev);
            ret.back().append("*");
        }
    }
    //remove duplicates
    std::sort(ret.begin(), ret.end());
    ret.erase(std::unique(ret.begin(), ret.end()), ret.end());
    return ret;
}

/** Returns what error message to emit, when a shape name is looked up.*/
std::pair<std::string, std::string> ShapeCollection::ErrorOnShapeName(std::string_view name) const
{
    if (shapes.size()==0) 
        return{"No shapes have been defined.",
            "Use the 'defshape' command to define shapes."};
    if (GetShapeNo(name)==-1) {
        string msg("Use one of '");
        const auto v = ShapeNames(name);
        for (size_t u = 0; u<v.size()-1; u++)
            if (u+2==v.size())
                msg.append(v[u]).append("' or '");
            else
                msg.append(v[u]).append("', '");
        msg.append(v.back());
        return{"Unrecognized shape '"+StringFormat::RemovePosEscapesCopy(name)+"'.", msg + "'."};
    }
    return{"",""};
}


int ShapeCollection::GetShapeNo(std::string_view sh_name) const
{
    for (unsigned u = 0; u < shapes.size(); u++)
        if (CaseInsensitiveEqual(shapes[u].name, sh_name))
        return u;
    return -1;
}

/** Return the coverage of the given shape or none if bad shape number
* @param [in] sh The number of the shape
* @param [in] o The boundaries into which the shape shall be fitted
* @returns the Contour representing the union of bg, fg_fill and fg_line sections*/
Contour ShapeCollection::Cover(unsigned sh, const Block &o) const
{
    if (sh >= shapes.size()) return Contour();
    const Block &max = shapes[sh].GetMax();
    Contour c = shapes[sh].GetCover();
    c.Scale(XY(o.x.Spans()/max.x.Spans(), o.y.Spans()/max.y.Spans()));
    c.Shift(XY(o.x.from - max.x.from*o.x.Spans()/max.x.Spans(), o.y.from - max.y.from*o.y.Spans()/max.y.Spans()));
    return c;
}




bool CshHintGraphicCallbackForPorts(Canvas *canvas, CshHintGraphicParam p, CshHintStore &csh)
{
    if (!csh.pShapes) return false;
    const size_t shape = size_t(p)/ShapePortCshHintGraphicCallbackMultiplier;
    const size_t port  = size_t(p)%ShapePortCshHintGraphicCallbackMultiplier;
    if (csh.pShapes->ShapeNum()<= shape) return false;
    Block hint = (*csh.pShapes)[shape].GetHintPos();
    if (hint.IsInvalid())
        hint = (*csh.pShapes)[shape].GetMax();
    const double r = std::min(HINT_GRAPHIC_SIZE_X/hint.x.Spans(), HINT_GRAPHIC_SIZE_Y/hint.y.Spans());
    //The origin of the part visible in the hint window in the space of the Shape
    const XY orig(hint.x.from + (hint.x.Spans() - HINT_GRAPHIC_SIZE_X/r)/2,
        hint.y.from + (hint.y.Spans() - HINT_GRAPHIC_SIZE_Y/r)/2);
    Block max = (*csh.pShapes)[shape].GetMax();
    max.Shift(-orig).Scale(r);
    csh.pShapes->Draw(*canvas, shape, max, LineAttr(), FillAttr::None());
    auto pp = csh.pShapes->GetShape(shape)->GetPort(port, max);
    if (pp) {
        canvas->Fill(Contour(pp.value().xy, 2), FillAttr(ColorType::red(), EGradientType::OUTWARD));
        if (pp.value().dir>=0) {
            const XY dir = XY(0, -1).Rotate(cos(pp.value().dir/180*M_PI), sin(pp.value().dir/180*M_PI));
            canvas->Line(Path({pp.value().xy, pp.value().xy+dir}), LineAttr(ELineType::SOLID, ColorType::red()));
        }
    }
    return true;
}


