/*
    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 mscattribute.cpp The definition of note attributes.
 * @ingroup libmscgen_files */

#include "mscstyle.h"
#include "msc.h"

using namespace msc;

/** Make the style fully specified using default note style values.
* The default value is callout type, no specific position preference.
* Attributes already set are left unchanged.
* No change made to an already fully specified style.*/
void NoteAttr::MakeComplete()
{
    if (!pointer) { pointer = CALLOUT; }
    if (!def_float_dist) { def_float_dist = 0; }
    if (!def_float_x) { def_float_x = 0; }
    if (!def_float_y) { def_float_y = 0; }
    width.MakeComplete();
}

NoteAttr &NoteAttr::operator +=(const NoteAttr&a)
{
    if (a.pointer) pointer = a.pointer;
    if (a.def_float_dist) def_float_dist = a.def_float_dist;
    if (a.def_float_x) def_float_x = a.def_float_x;
    if (a.def_float_y) def_float_y = a.def_float_y;
    width = a.width;
    return *this;
};

bool NoteAttr::operator == (const NoteAttr &a) const
{
    if (a.pointer != pointer) return false;
    if (a.def_float_dist != def_float_dist) return false;
    if (a.def_float_x != def_float_x) return false;
    if (a.def_float_y != def_float_y) return false;
    if (a.width != width) return false;
    return true;
}

/** Take an attribute and apply it to us.
*
* We consider attributes ending with 'width', 'pointer' and 'pos';
* or any style at the current context in `chart`. We also accept the clearing of
* an attribute if `t` is EStyleType::STYLE, that is for style definitions only.
* At a problem, we generate an error into chart->Error.
* @param [in] a The attribute to apply.
* @param chart The chart we build.
* @param [in] t The situation we set the attribute.
* @returns True, if the attribute was recognized as ours (may have been a bad value though).*/
bool NoteAttr::AddAttribute(const Attribute &a, MscChart *chart, EStyleType t)
{
    if (a.type == EAttrType::STYLE) {
        if (chart->MyCurrentContext().styles.find(a.name) == chart->MyCurrentContext().styles.end()) {
            a.InvalidStyleError(chart->Error);
            return true;
        }
        const MscStyle &style = dynamic_cast<const MscStyle&>(chart->MyCurrentContext().styles[a.name].read());
        if (style.f_note) operator +=(style.note);
        return true;
    }
    if (a.EndsWith("pointer")) {
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t))
                pointer.reset();
            return true;
        }
        if (NoteAttr::EPointerType t;  a.type == EAttrType::STRING && Convert(a.value, t)) {
            pointer = t;
            return true;
        }
        a.InvalidValueError(CandidatesFor(*pointer), chart->Error);
        return true;
    }
    if (a.EndsWith("pos")) {
        if (a.type == EAttrType::CLEAR) {
            if (a.EnsureNotClear(chart->Error, t)) {
                def_float_dist.reset();
                def_float_x.reset();
                def_float_y.reset();
            }
            return true;
        }
        EPosType tmp;
        if (a.type == EAttrType::STRING && Convert(a.value, tmp)) {
            switch (tmp) {
            default:
            case POS_INVALID: _ASSERT(0); break;
            case POS_NEAR:   def_float_dist = -1; break;
            case POS_FAR:    def_float_dist = +1; break;
            case LEFT:       def_float_x = -1; def_float_y = 0; break;
            case RIGHT:      def_float_x = +1; def_float_y = 0; break;
            case UP:         def_float_x = 0; def_float_y = -1; break;
            case DOWN:       def_float_x = 0; def_float_y = +1; break;
            case LEFT_UP:    def_float_x = -1; def_float_y = -1; break;
            case LEFT_DOWN:  def_float_x = -1; def_float_y = +1; break;
            case RIGHT_UP:   def_float_x = +1; def_float_y = -1; break;
            case RIGHT_DOWN: def_float_x = +1; def_float_y = +1; break;
            }
            return true;
        }
        a.InvalidValueError(CandidatesFor(POS_INVALID), chart->Error);
        return true;
    }
    if (width.AddAttribute(a, chart, t))
        return true;
    return false;
}

/** Add the attribute names we take to `csh`.*/
void NoteAttr::AttributeNames(Csh &csh)
{
    static const char *const names_descriptions[] =
    {"", nullptr,
        "note.pointer", "Select what kind of pointer points to the noted element.",
        "note.pos", "Influence where the note is placed.",
        "note.width", "Select the width of the note. Useful if you want the text to wrap inside it (via 'text.warp=yes').",
        ""};
    csh.AddToHints(names_descriptions, csh.HintPrefix(COLOR_ATTRNAME), EHintType::ATTR_NAME);
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME)+"note.*",
        "Adust the pointer, position and size of the note.",
        EHintType::ATTR_NAME));
}

/** Possible values for a note pointer type*/
template<> const char EnumEncapsulator<NoteAttr::EPointerType>::names[][ENUM_STRING_LEN] =
{"invalid", "none", "callout", "arrow", "blockarrow", ""};

/** Possible values for a note pos attribute*/
template<> const char EnumEncapsulator<NoteAttr::EPosType>::names[][ENUM_STRING_LEN] =
{"invalid", "near", "far", "left", "right", "up", "down", "left_up", "left_down", "right_up", "right_down", ""};
template<> const char *const EnumEncapsulator<NoteAttr::EPosType>::descriptions[] =
{nullptr, "Try placing the note close to its target.", "Try placing the note far from its target.",
"Try placing the note at the left side of its target.",
"Try placing the note at the right side of its target.",
"Try placing the note above its target.",
"Try placing the note below its target.",
"Try placing the note left and up from its target.",
"Try placing the note left and down from its target.",
"Try placing the note right and up from its target.",
"Try placing the note right and down from its target.",
""};

/** Add a list of possible attribute value names to `csh` for attribute `attr`.*/
bool NoteAttr::AttributeValues(std::string_view attr, Csh &csh)
{
    if (CaseInsensitiveEndsWith(attr, "pointer")) {
        csh.AddToHints(EnumEncapsulator<NoteAttr::EPointerType>::names, nullptr, csh.HintPrefix(COLOR_ATTRVALUE),
            EHintType::ATTR_VALUE, CshHintGraphicCallbackForPointer);
        return true;
    }
    if (CaseInsensitiveEndsWith(attr, "pos")) {
        csh.AddToHints(EnumEncapsulator<NoteAttr::EPosType>::names,
            EnumEncapsulator<NoteAttr::EPosType>::descriptions,
            csh.HintPrefix(COLOR_ATTRVALUE),
            EHintType::ATTR_VALUE, CshHintGraphicCallbackForPos);
        return true;
    }
    if (WidthAttr::AttributeValues(attr, csh)) return true;
    return false;
}

/** Callback for drawing a symbol before note pointer type names in the hints popup list box.
* @ingroup hintpopup_callbacks*/
bool NoteAttr::CshHintGraphicCallbackForPointer(Canvas *canvas, CshHintGraphicParam p, CshHintStore &csh)
{
    if (!canvas) return false;
    const NoteAttr::EPointerType v = NoteAttr::EPointerType(p);
    if (v!=NoteAttr::ARROW && v!=NoteAttr::BLOCKARROW &&
        v!=NoteAttr::NONE && v!=NoteAttr::CALLOUT)
        return false;
    const Block object(HINT_GRAPHIC_SIZE_X*0.7, HINT_GRAPHIC_SIZE_X, 0, HINT_GRAPHIC_SIZE_Y);
    const FillAttr object_fill(ColorType(128, 128, 128), EGradientType::LEFT);
    const LineAttr object_line;
    Contour note;
    if (v==NoteAttr::CALLOUT) {
        const XY points[] = {XY(HINT_GRAPHIC_SIZE_X*0.7, HINT_GRAPHIC_SIZE_Y*0.6),
            XY(HINT_GRAPHIC_SIZE_X*0.2, HINT_GRAPHIC_SIZE_Y*0.3),
            XY(HINT_GRAPHIC_SIZE_X*0.2, HINT_GRAPHIC_SIZE_Y*0.0),
            XY(HINT_GRAPHIC_SIZE_X*0.0, HINT_GRAPHIC_SIZE_Y*0.0),
            XY(HINT_GRAPHIC_SIZE_X*0.0, HINT_GRAPHIC_SIZE_Y*1.0),
            XY(HINT_GRAPHIC_SIZE_X*0.2, HINT_GRAPHIC_SIZE_Y*1.0),
            XY(HINT_GRAPHIC_SIZE_X*0.2, HINT_GRAPHIC_SIZE_Y*0.5),
            XY(HINT_GRAPHIC_SIZE_X*0.7, HINT_GRAPHIC_SIZE_Y*0.6)};
        note = Contour(points);
    } else {
        note = Block(0, HINT_GRAPHIC_SIZE_X*0.2, 0, HINT_GRAPHIC_SIZE_Y);
    }
    canvas->Clip(XY(1, 1), XY(HINT_GRAPHIC_SIZE_X-1, HINT_GRAPHIC_SIZE_Y-1));
    //draw object we comment
    canvas->Fill(object, object_fill);
    canvas->Line(object.UpperLeft(), object.LowerLeft(), object_line);
    //draw note
    const LineAttr line(ELineType::SOLID, ColorType(0, 192, 32), 1, ECornerType::NONE, 0); //green-blue
    const FillAttr fill = FillAttr::Solid(line.color->Lighter(0.7));
    const ShadowAttr shadow;
    canvas->Shadow(note, shadow);
    canvas->Fill(note, fill);
    canvas->Line(note, line);
    //draw arrow
    switch (v) {
    case NoteAttr::ARROW:
        canvas->Clip(Block(HINT_GRAPHIC_SIZE_X*0.2-1, HINT_GRAPHIC_SIZE_X, 0, HINT_GRAPHIC_SIZE_Y));
        CshHintGraphicCallbackForArrows(canvas, EArrowType::SOLID, EArrowSize::SMALL, false);
        canvas->UnClip();
        break;
    case NoteAttr::BLOCKARROW:
        canvas->Clip(Block(HINT_GRAPHIC_SIZE_X*0.2-1, HINT_GRAPHIC_SIZE_X, 0, HINT_GRAPHIC_SIZE_Y));
        CshHintGraphicCallbackForBigArrows(canvas, (int)EArrowType::SOLID, csh);
        canvas->UnClip();
        break;
    default:
        break;
    }
    canvas->UnClip();
    return true;
}

/** Callback for drawing a symbol before names of values for the note positions in the hints popup list box.
* @ingroup hintpopup_callbacks*/
bool NoteAttr::CshHintGraphicCallbackForPos(Canvas *canvas, CshHintGraphicParam p, CshHintStore &)
{
    if (!canvas) return false;
    double dist = 1;
    XY pos(+1, +1);
    switch (NoteAttr::EPosType(int(p))) {
    default:
    case NoteAttr::POS_INVALID: return false;
    case NoteAttr::POS_NEAR:   dist = 0.8; break;
    case NoteAttr::POS_FAR:    dist = 1.2; break;
    case NoteAttr::LEFT:       pos.x = -1; pos.y = 0; break;
    case NoteAttr::RIGHT:      pos.x = +1; pos.y = 0; break;
    case NoteAttr::UP:         pos.x = 0; pos.y = -1; break;
    case NoteAttr::DOWN:       pos.x = 0; pos.y = +1; break;
    case NoteAttr::LEFT_UP:    pos.x = -1; pos.y = -1; break;
    case NoteAttr::LEFT_DOWN:  pos.x = -1; pos.y = +1; break;
    case NoteAttr::RIGHT_UP:   pos.x = +1; pos.y = -1; break;
    case NoteAttr::RIGHT_DOWN: pos.x = +1; pos.y = +1; break;
    }

    const double r0 = 0.4; //object rectangle offset
    const double r1 = 0.3; //object rectangle halfsize
    const double r2 = 0.15; //note rectangle halfsize
    const double r3 = 0.65; //note rectangle midpoint offset

    canvas->Clip(XY(1, 1), XY(HINT_GRAPHIC_SIZE_X-1, HINT_GRAPHIC_SIZE_Y-1));
    //the center of the object
    const XY center(HINT_GRAPHIC_SIZE_X*(0.5-pos.x*r0), HINT_GRAPHIC_SIZE_Y*(0.5-pos.y*r0));
    //the tip of the note (on the contour of the object)
    const XY tip = XY(pos.x*HINT_GRAPHIC_SIZE_X*r1, pos.y*HINT_GRAPHIC_SIZE_Y*r1) + center;
    //the center of the note box
    const XY ori = XY(pos.x*HINT_GRAPHIC_SIZE_X*r3, pos.y*HINT_GRAPHIC_SIZE_Y*r3)*dist + center;
    //the halfsize of the note box
    const XY wh = XY(HINT_GRAPHIC_SIZE_X*r2, HINT_GRAPHIC_SIZE_Y*r2);
    //the offset of the pointer's base from "ori" (1.5 pixels)
    const XY para = (ori-center).Rotate90CW().Normalize()*1.5;

    //Draw object
    const Contour object = Contour(-HINT_GRAPHIC_SIZE_X*r1, HINT_GRAPHIC_SIZE_X*r1,
        -HINT_GRAPHIC_SIZE_Y*r1, HINT_GRAPHIC_SIZE_Y*r1).Shift(center);
    canvas->Fill(object, FillAttr::Solid(ColorType(128, 128, 128)));
    canvas->Line(object, LineAttr());
    //Draw note
    const Contour c = Contour(Block(ori-wh, ori+wh)) + Contour(tip, ori+para, ori-para);
    const LineAttr line(ELineType::SOLID, ColorType(0, 192, 32), 1, ECornerType::NONE, 0); //green-blue
    const FillAttr fill = FillAttr::Solid(line.color->Lighter(0.7));
    const ShadowAttr shadow;
    canvas->Shadow(c, shadow);
    canvas->Fill(c, fill);
    canvas->Line(c, line);
    canvas->UnClip();
    return true;
}


/** Print the line style to a string.*/
string NoteAttr::Print(int) const
{
    string ss = "note(";
    if (pointer) ss << " pointer:" << EnumEncapsulator<NoteAttr::EPointerType>::names[*pointer];
    if (def_float_dist) ss << " def_float_dist:" << *def_float_dist;
    if (def_float_x) ss << " def_float_x:" << *def_float_x;
    if (def_float_y) ss << " def_float_y:" << *def_float_y;
    return ss + ")";
}
