/*
    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 style.cpp The implementation of styles (Style and SimpleStyle) and contexts (Context).
 * @ingroup libcgencommon_files */

#include "style.h"
#include "chartbase.h"


bool Style::AddAttribute(const Attribute &a, Chart*chart)
{
    if (a.type == EAttrType::STYLE) {
        const Style *s = chart->GetCurrentContext()->GetStyle4Read(a.name);
        if (s)
            operator += (*s);
        else
            a.InvalidStyleError(chart->Error);
        return true;
    }
    return false;
}


/** Create an empty style that contains all possible attributes.*/
SimpleStyle::SimpleStyle(EStyleType tt, EColorMeaning cm) :
    Style(tt)
{
    color_meaning = cm;
    f_line=f_fill=f_shadow=f_text=true;
    f_numbering=true;
    f_shape = true;
    Empty();
}

/** Create an empty style that contains only some of the attributes.
 * @param [in] tt The type of style instance.
 * @param [in] cm How an unqualified "color" attribute shall be interpreted.
 * @param [in] t True if the style shall contain text attributes.
 * @param [in] l True if the style shall contain line attributes.
 * @param [in] f True if the style shall contain fill attributes.
 * @param [in] s True if the style shall contain shadow attributes.
 * @param [in] nu True if the style shall contain the `number` attribute.
 * @param [in] shp True if the style shall contain entity related attributes (shape, shape.size)
 */
SimpleStyle::SimpleStyle(EStyleType tt, EColorMeaning cm,
             bool t, bool l, bool f, bool s, bool nu, bool shp) :
    Style(tt), color_meaning(cm),
    f_line(l), f_fill(f), f_shadow(s),
    f_text(t), f_numbering(nu), f_shape(shp)
{
    Empty();
}

/** Make a style complete by setting the default values - but leave text & lost attributes empty.
 * Default attributes are half solid, right side, no numbering, no compress, indicator yes,
 * makeroom yes and the default of other attribute classes (specified there).
 * We skip text styles, since we have a global text attribute per chart and
 * we set the default there. We also skip loss attributes as they do not have to be
 * fully specified.
 */
void SimpleStyle::MakeCompleteButText()
{
    Style::MakeCompleteButText();
    if (f_line) line.MakeComplete();
    else line.Empty();
    if (f_fill) fill.MakeComplete();
    else fill.Empty();
    if (f_shadow) shadow.MakeComplete();
    else shadow.Empty();
    //text untouched
    if (f_numbering) numbering = false;
    shape = -1; //no shape
    shape_size = EArrowSize::SMALL;
}

/** Make the style empty be unsetting all attributes it contains.*/
void SimpleStyle::Empty()
{
    Style::Empty();
    line.Empty();
    fill.Empty();
    text.Empty();
    shadow.Empty();
    numbering.reset();
    shape.reset();
    shape_size.reset();
}

bool SimpleStyle::IsEmpty() const noexcept {
    return Style::IsEmpty()
        && (!f_line || line.IsEmpty())
        && (!f_fill || fill.IsEmpty())
        && (!f_text || text.IsEmpty())
        && (!f_shadow || shadow.IsEmpty())
        && (!f_numbering || !numbering)
        && (!f_shape || (!shape && !shape_size));
}


/** Merge another style to us by copying the value of those attributes which are contained by both and are set in `toadd`.*/
Style & SimpleStyle::operator +=(const Style &toadd)
{
    Style::operator+=(toadd);
    const SimpleStyle *p = dynamic_cast<const SimpleStyle*>(&toadd);
    if (p==nullptr)
        return *this;
    if (p->f_line && f_line) line += p->line;
    if (p->f_fill && f_fill) fill += p->fill;
    if (p->f_shadow &&f_shadow) shadow += p->shadow;
    if (p->f_text && f_text) text += p->text;
    if (p->f_numbering && f_numbering && p->numbering) numbering = p->numbering;
    if (p->f_shape && f_shape) {
        if (p->shape) shape = p->shape;
        if (p->shape_size) shape_size = p->shape_size;
    }
    return *this;
}

/** Apply an attribute to us.
 * Generate an error if the we recognize the attribute, but bad value.
 * Do not recognize attribute names that correspond to attributes we do not contain.*/
bool SimpleStyle::AddAttribute(const Attribute &a, Chart*chart)
{
    if (a.Is("color")) {
        unsigned num = 0;
        switch (color_meaning) {
        case EColorMeaning::LINE_ARROW_TEXT:
            if (f_line)
                num += line.AddAttribute(a, chart, type);
            if (f_text)
                num += text.AddAttribute(a, chart, type);
            return num!=0;
        case EColorMeaning::LINE_VLINE_TEXT:
            if (f_line)
                num += line.AddAttribute(a, chart, type);
            if (f_text)
                num += text.AddAttribute(a, chart, type);
            return num!=0;
        case EColorMeaning::TEXT:
        case EColorMeaning::ARROW_TEXT:
            if (f_text)
                return text.AddAttribute(a, chart, type);
            return false;
        case EColorMeaning::FILL:
            if (f_fill)
                return fill.AddAttribute(a, chart, type);
            return false;
        case EColorMeaning::LINE_FILL:
            if (f_line)
                num += line.AddAttribute(a, chart, type);
            if (f_fill)
                num += fill.AddAttribute(a, chart, type);
            return num != 0;
        default:
            _ASSERT(0);
            FALLTHROUGH;
        case EColorMeaning::NOHOW:
            return false;
        }
    }
    if (f_text) {
        if (a.StartsWith("text") || a.Is("ident"))
            return text.AddAttribute(a, chart, type);
    }
    if (f_line) {
        if (a.StartsWith("line"))
            return line.AddAttribute(a, chart, type);
    }
    if (f_fill && a.StartsWith("fill"))
        return fill.AddAttribute(a, chart, type);
    if (f_shadow && a.StartsWith("shadow"))
        return shadow.AddAttribute(a, chart, type);
    if (a.Is("number") && f_numbering) {
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, type))
                numbering.reset();
            return true;
        }
        if (a.type != EAttrType::BOOL) {
            chart->Error.Error(a, true, "The 'number' attribute must be 'yes' or 'no' for styles. Ignoring it.");
            return true;
        }
        numbering = a.yes;
        return true;
    }
    if (f_shape) {
        if (a.Is("shape")) {
            if (chart->SkipContent())
                return true;
            if (a.type == EAttrType::CLEAR) {
                shape.reset();
                return true;
            }
            if (!a.CheckType(EAttrType::STRING, chart->Error))
                return true;
            auto msg = chart->Shapes.ErrorOnShapeName(a.value);
            if (msg.first.length()) {
                chart->Error.Error(a, true, msg.first+" Ignoring attribute.", msg.second);
            } else {
                shape = chart->Shapes.GetShapeNo(a.value);
                _ASSERT(*shape>=0);
            }
            return true;
        }
        if (a.Is("shape.size")) {
            if (!chart->Shapes) {
                chart->Error.Error(a, false, "No shapes have been defined. Ignoring attribute.",
                    "Use the 'defshape' command to define shapes.");
                return true;
            }
            if (a.type == EAttrType::CLEAR) {
                shape_size.reset();
                return true;
            }
            if (EArrowSize s; a.type == EAttrType::STRING && Convert(a.value, s)) {
                shape_size = s;
                return true;
            } else
                a.InvalidValueError(CandidatesFor<EArrowSize>(), chart->Error);
            return true;
        }
    }
    return Style::AddAttribute(a, chart);  //adds styles
}

bool SimpleStyle::DoIAcceptUnqualifiedColorAttr() const
{
    switch (color_meaning) {
    case EColorMeaning::FILL: return f_fill;
    case EColorMeaning::ARROW_TEXT:
    case EColorMeaning::TEXT: return f_text;
    case EColorMeaning::LINE_ARROW_TEXT:
        return f_text || f_line;
    case EColorMeaning::LINE_VLINE_TEXT:
        return f_text || f_line;
    case EColorMeaning::LINE_FILL:
        return f_fill || f_line;
    default:
        _ASSERT(0);
        FALLTHROUGH;
    case EColorMeaning::NOHOW:
        return false;
    }
}


/** Add the attribute names we take to `csh`.*/
void SimpleStyle::AttributeNames(Csh &csh) const
{
    if (DoIAcceptUnqualifiedColorAttr())
        csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME)+"color",
          "Set the color of the element.",
          EHintType::ATTR_NAME));
    if (f_line) {
        LineAttr::AttributeNames(csh, "line.");
        csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME)+"line.*",
            "Options for the line style of the element.",
            EHintType::ATTR_NAME));
    }
    if (f_fill) {
        FillAttr::AttributeNames(csh, "fill.");
        csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME)+"fill.*",
            "Options for how to fill the inside of the element.",
            EHintType::ATTR_NAME));
    }
    if (f_shadow) ShadowAttr::AttributeNames(csh);
    if (f_text) {
        StringFormat::AttributeNames(csh, "text.");
        csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME)+"text.*",
            "Adjust the format of the label text.",
            EHintType::ATTR_NAME));
    }
    if (f_numbering) csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME)+"number",
        "Turn numberin off or specify a concrete number.",
        EHintType::ATTR_NAME));
    if (f_shape && csh.pShapes && *csh.pShapes) {
        csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "shape",
            "Set the shape of the entity.",
            EHintType::ATTR_NAME));
        csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "shape.size",
            "Set the shape size of the entity.",
            EHintType::ATTR_NAME));
    }
    Style::AttributeNames(csh);  //adds style names
}

/** Add a list of possible attribute value names to `csh` for attribute `attr`.*/
bool SimpleStyle::AttributeValues(std::string_view attr, Csh &csh) const
{
    if (CaseInsensitiveEqual(attr, "color")) {
        if (!DoIAcceptUnqualifiedColorAttr()) return false;
        csh.AddColorValuesToHints(false);
        return true;
    }
    if (f_text && (CaseInsensitiveBeginsWith(attr, "text") || CaseInsensitiveEqual(attr, "ident")))
        return text.AttributeValues(attr, csh);
    if (CaseInsensitiveBeginsWith(attr, "line") && f_line)
        return line.AttributeValues(attr, csh);
    if (CaseInsensitiveBeginsWith(attr, "fill") && f_fill)
        return fill.AttributeValues(attr, csh);
    if (CaseInsensitiveBeginsWith(attr, "shadow") && f_shadow)
        return shadow.AttributeValues(attr, csh);
    if (CaseInsensitiveEqual(attr, "number") && f_numbering) {
        csh.AddToHints(CshHint(csh.HintPrefixNonSelectable() + "<number>",
            "Specify a concrete number to set the number of the element.",
            EHintType::ATTR_VALUE, false));
        csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRVALUE)+"yes",
            "Turn on auto-numbering for this element.",
            EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForYesNo, CshHintGraphicParam(1)));
        csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRVALUE)+"no",
            "Exempt this element from auto-numbering and have no number here.",
            EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForYesNo, CshHintGraphicParam(0)));
        return true;
    }
    if (f_shape) {
        if (csh.pShapes && *csh.pShapes && CaseInsensitiveEqual(attr, "shape")) {
            csh.AddShapesToHints();
            return true;
        }
        if (CaseInsensitiveEqual(attr, "shape.size")) {
            FullArrowAttr::AttributeValues(attr, csh, EArcArrowType::ANY);
            return true;
        }
    }
    return Style::AttributeValues(attr, csh);
}

/** Apply our text attributes on top of 'sf' and make that
 * our text attribute. In other words insert the content of
 * 'sf' *before* the attrs in our text.*/
void SimpleStyle::ApplyTextBefore(const StringFormat& sf) {
    if (text.IsEmpty())
        text = sf;
    else {
        StringFormat sf_ = sf;
        sf_ += text;
        text = sf_;
    }
}

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

/** Returns the first matching parameter pair, null-null, if none*/
std::pair<const ProcParamDef*, const ProcParamDef*> Procedure::AreAllParameterNamesUnique(const ProcParamDefList &params)
{
    for (auto i = params.begin(); i!=params.end(); i++)
        for (auto j = std::next(i); j!=params.end(); j++)
            if ((*i)->name==(*j)->name) return{i->get(), j->get()};
    return{nullptr,nullptr};
}

const ProcParamDef * Procedure::GetProcParamDef(std::string_view name) const
{
    for (auto &p : parameters)
        if (p->name==name) return p.get();
    return nullptr;
}

/** Consume a parameter list from procedure invocation and match them to our
 * parameter list. Report errors to chart & return empty on error.
 * Also adds a $$ parameter with the location of invocation.*/
std::optional<ParameterContext> Procedure::MatchParameters(const ProcParamInvocationList& pl,
                                                           const FileLineCol& invocation, Chart* chart) const
{
    std::optional<ParameterContext> ret{std::in_place};
    const bool has_params_in_invocation = pl.size();
    auto my_i = parameters.begin();
    auto i = pl.begin();
    for (; i!=pl.end() && my_i != parameters.end(); i++, my_i++) {
        if ((*i)->has_value) {
            if (ret)
                ret->emplace((*my_i)->name, ProcParamResolved((*i)->value, (*i)->linenum_value, true));
        } else {
            if ((*my_i)->has_default_value) {
                if (ret)
                    ret->emplace((*my_i)->name, ProcParamResolved { (*my_i)->default_value, (*my_i)->linenum_default_value, true });
            } else {
                chart->Error.Error((*i)->linenum_value, "This parameter has no default and requires a value. Ignoring procedure call.");
                chart->Error.Error((*my_i)->linenum_name, (*i)->linenum_value, "Here is the definition of this parameter.");
                ret.reset();
            }
        }
    }
    if (i!=pl.end()) {
        if (parameters.size())
            chart->Error.Error((*i)->linenum_value, "Procedure "+name+" takes only " + std::to_string(parameters.size()) +" parameters. Ignoring procedure call.");
        else
            chart->Error.Error((*i)->linenum_value, "Procedure "+name+" takes no parameters. Ignoring procedure call.");
        ret.reset();
    }
    for (; my_i != parameters.end(); my_i++) {
        if ((*my_i)->has_default_value) {
            if (ret)
                ret->emplace((*my_i)->name, ProcParamResolved { (*my_i)->default_value, (*my_i)->linenum_default_value, true });
        } else if (has_params_in_invocation) {
            chart->Error.Error(invocation, "Procedure "+name+" has parameters that have no default and therefore require values. Ignoring procedure call.");
            chart->Error.Error((*my_i)->linenum_name, invocation, "Here is the definition of such a parameter.");
            ret.reset();
            break; //report only once
        } else {
            chart->Error.Error(invocation, "Parameter "+(*my_i)->name+" has no default and requires a value. Ignoring procedure call.");
            chart->Error.Error((*my_i)->linenum_name, invocation, "Here is the definition of this parameter.");
            ret.reset();
        }
    }
    if (ret) {
        //if OK, expand any references in the parameter values - but keep the hidden ones so that $$ notion is kept
        //This is not strictly OK - we cannot know if this value will be used as a label, formatting or number format.
        //(if it is neither, then it will contain no formatting escapes, so the below function does nothing.)
        //But using it as a number format is probably something never happens...
        for (auto& p : *ret)
            StringFormat::ExpandReferences(p.second.value, chart, p.second.linenum_value, nullptr,
                                           false, true, StringFormat::ETextType::LABEL, false);
        //add $$ parameter as a unique value to this invocation (in a HIDDEN escape)
        std::string v = invocation.Print();
        v[1] = ESCAPE_CHAR_HIDDEN;
        ret->emplace(std::piecewise_construct, std::forward_as_tuple("$$"), std::forward_as_tuple(v, invocation, true));
    }
    return ret;
}

void Procedure::AddAttribute(const Attribute & a, Chart & chart)
{
    if (a.Is("export")) {
        if (a.type == EAttrType::CLEAR) {
            chart.Error.Error(a, false, "You cannot unset this attribute. Use 'yes' or 'no'. Ignoring it.");
            return;
        }
        if (a.type != EAttrType::BOOL) {
            chart.Error.Error(a, true, "The 'export' attribute must be 'yes' or 'no' for styles. Ignoring it.");
            return;
        }
        export_styles = export_colors = a.yes;
        return;
    }
    return;
}

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

void Context::Plain()
{
    is_full = true;
    text.Default();
    colors.clear();
    colors["none"] = ColorType(0, 0, 0, 0);
    //W3C colors
    colors["black"] = ColorType(0, 0, 0);
    colors["white"] = ColorType(255, 255, 255);
    colors["red"] = ColorType(255, 0, 0);
    colors["green"] = ColorType(0, 255, 0);
    colors["blue"] = ColorType(0, 0, 255);
    colors["yellow"] = ColorType(255, 255, 0);
    colors["gray"] = ColorType(128, 128, 128);
    colors["silver"] = ColorType(0xc0, 0xc0, 0xc0);
    colors["maroon"] = ColorType(0x80, 0x00, 0x00);
    colors["orange"] = ColorType(0xff, 0xb0, 0x00);
    colors["olive"] = ColorType(0x80, 0x80, 0x00);
    colors["lime"] = ColorType(0, 255, 0);
    colors["aqua"] = ColorType(0, 255, 255);
    colors["teal"] = ColorType(0, 128, 128);
    colors["navy"] = ColorType(0, 0, 128);
    colors["indigo"] = ColorType(0x44, 0x00, 0x88);
    colors["purple"] = ColorType(128, 0, 128);
    colors["violet"] = ColorType(0xd0, 0x20, 0x90);
    colors["fuchsia"] = ColorType(0xff, 0x00, 0xff);
    //Additional colors
    colors["lgray"] = ColorType(200, 200, 200);
}

void Context::ApplyContextContent(const Context &o)
{
    //There is nothing in our ancestor ContextParam to apply
    if (o.is_full) {
        is_full = true;
        text = o.text;
        colors = o.colors;
        Procedures = o.Procedures;
        parameters.clear(); //will copy content below
    } else {
        text += o.text;
        colors += o.colors;
        for (auto &pp : o.Procedures)
            Procedures[pp.first] = pp.second;
    }
    //Copy only the variables, not the parameters
    for (auto &pp : o.parameters)
        if (!pp.second.is_parameter)
            parameters.emplace(pp);
}

void Context::ApplyContextContent(Context &&o)
{
    //There is nothing in our ancestor ContextParam to apply
    if (o.is_full) {
        is_full = true;
        text = std::move(o.text);
        colors = std::move(o.colors);
        Procedures = std::move(o.Procedures);
        parameters.clear(); //will copy content below
    } else {
        text += o.text;
        colors += o.colors;
        o.Procedures.insert(Procedures.begin(), Procedures.end()); //Will NOT insert procedures already existing in o.Procedures
        o.Procedures.swap(Procedures);
    }
    //Copy only the variables, not the parameters
    for (auto &pp : o.parameters)
    if (!pp.second.is_parameter)
        parameters.emplace(std::move(pp));
}

