/*
    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 element.cpp The implementation of class Element.
 * @ingroup libcgencommon_files */

#define _CRT_NONSTDC_NO_DEPRECATE

#include "chartbase.h"
using namespace std;


constexpr XY Element::control_size;
constexpr XY Element::indicator_size;

Element::Element() noexcept : 
    linenum_final(false), draw_is_different(false), area_draw_is_frame(false)
{
    area.arc = this;
    control_location.MakeInvalid();
}

/** A non-default copy constructor, area.arc is changed*/
Element::Element(const Element&o) :
    linenum_final(o.linenum_final),
    area(o.area), area_draw(o.area_draw),
    draw_is_different(o.draw_is_different), area_draw_is_frame(o.area_draw_is_frame), 
    area_to_note(o.area_to_note),
    area_important(o.area_important), 
    controls(o.controls), control_location(o.control_location),
    file_pos(o.file_pos)
{
    area.arc = this;
}

/** A non-default copy constructor, area.arc is changed*/
Element::Element(Element&&o) noexcept :
    linenum_final(std::move(o.linenum_final)),
    area(std::move(o.area)), area_draw(std::move(o.area_draw)),
    draw_is_different(std::move(o.draw_is_different)), area_draw_is_frame(std::move(o.area_draw_is_frame)),
    area_to_note(std::move(o.area_to_note)),
    area_important(std::move(o.area_important)),
    controls(std::move(o.controls)), control_location(std::move(o.control_location)),
    file_pos(std::move(o.file_pos))
{
    area.arc = this;
}

/** Record the location of the element in the input file
 * @param [in] l The range the element occupies in the input file to record.
 * @param [in] f If true, the recording is final - any more calls to 
 * SetLineEnd()/ExpandLineEnd() will be ignored.*/
void Element::SetLineEnd(FileLineColRange l, bool f)
{
    if (linenum_final) return;
    linenum_final = f;
    file_pos = l;
}

/** Expand the location of the element in the input file by adding l to 'file_pos'.
* @param [in] l The range the element occupies in the input file to expand with.
* @param [in] f If true, the recording is final - any more calls to 
* SetLineEnd()/ExpandLineEnd() will be ignored.*/
void Element::ExpandLineEnd(FileLineColRange l, bool f)
{
    if (linenum_final) return;
    linenum_final = f;
    if (file_pos.IsInvalid())
        file_pos = l;
    else {
        if (l.start < file_pos.start) file_pos.start = l.start;
        if (l.end < file_pos.end) file_pos.end = l.end;
    }
}




/** Shift an element up or down 
 * Shifts all `area_*` members, `yPos`, `control_location` and
 * all our comments;*/
void Element::ShiftBy(double y)
{
    if (y==0) return;
    area.Shift(XY(0, y));
    area_draw.Shift(XY(0, y));
    area_to_note.Shift(XY(0,y));
    area_to_note2.Shift(XY(0,y));
    area_important.Shift(XY(0,y)); 
    control_location.y += y;
}

/** Shift an element in any direction.
* Shifts all `area_*` members, `yPos`, `control_location` and
* all our comments;*/
void Element::ShiftBy(const XY &xy)
 {
     if (xy.y==0 && xy.x==0) return;
     area.Shift(xy);
     area_draw.Shift(xy);
     area_to_note.Shift(xy);
     area_to_note2.Shift(xy);
     area_important.Shift(xy);
     control_location += xy;
 }


/** Do processing after our positioning on the chart is known.
 * We expand `area` and `area_draw` by `chart->trackExpandBy`
 * if we show; set `control_location`.
 * We do nothing if `chart->prepare_for_tracking` is false.*/
void Element::PostPosProcess(Canvas &, Chart *chart)
{
    area.arc = this;
    if (!chart->prepare_for_tracking) {
        controls.clear();
        return;
    }
    if (area.IsEmpty()) {
        controls.clear();
    } else {
        //Determine, where the controls shall be shown
        //In case the area is non-rectangular (e.g., a large circle), we must position the controls
        //to a place where they are reachable by the mouse from the area.
        //Take the upper region of the area (as high as the controls would be) and place the
        //controls right of it.
        Block upper_region = area.GetBoundingBox();
        upper_region.y.till = std::min(upper_region.y.till, upper_region.y.from + control_size.y*controls.size());
        const double X = (area*upper_region).GetBoundingBox().x.till;
        const double Y = upper_region.y.from;
        control_location.x = Range(X, X + control_size.x);
        control_location.y = Range(Y, Y + control_size.y*controls.size());
        //Expand the area if the chart wants it. => For tracking we use a slightly larger area.
        if (chart->trackExpandBy) {
            area.Simplify();
            //If expand cannot do it,/we still keep the original stuff.
            Area expanded_area = area.CreateExpand(chart->trackExpandBy,
                                                   contour::EXPAND_MITER_ROUND,
                                                   contour::EXPAND_MITER_ROUND,
                                                   2, 2);
            _ASSERT(!expanded_area.IsEmpty());
            if (!expanded_area.IsEmpty()) area = std::move(expanded_area);
        }
    }
    if (draw_is_different && !area_draw.IsEmpty() && !area_draw_is_frame && chart->trackExpandBy) {
        area_draw.Simplify();
        area_draw = area_draw.CreateExpand(chart->trackExpandBy,
                                           contour::EXPAND_MITER_ROUND,
                                           contour::EXPAND_MITER_ROUND,
                                           2, 2);
    }
    if (!file_pos.IsInvalid())
        chart->AllElements[file_pos] = this;
}

/** Draw a single GUI control at (0,0) to a cairo context. 
 * @param cr The cairo context to draw onto.
 * @param [in] type The type of control to draw. We ASSERT a valid type,
 *             on invalid type we don't draw.
 * @param [in] size The scale factor [0.01..1]. When 1 we draw Element::control_size.*/
void Element::DrawControl(cairo_t *cr, EGUIControlType type, double size) {
    cairo_save(cr);
    cairo_translate(cr, control_size.x/2, control_size.y/2); //we draw centered at (0,0) below
    cairo_scale(cr, size, size);
    LineAttr l_rect(ELineType::SOLID, ColorType(0, 0, 0), 2, ECornerType::ROUND, (control_size.x+control_size.y)/10);
    FillAttr f_rect(ColorType(0, 0, 0), ColorType(64, 64, 64), EGradientType::DOWN);
    ShadowAttr s_rect(ColorType(0, 0, 0));
    s_rect.offset = 5;
    s_rect.blur = 5;
    Contour rect = l_rect.CreateRectangle_Midline(-control_size.x/2.1, control_size.x/2.1,
                                                  -control_size.y/2.1, control_size.y/2.1);
    cairo_set_source_rgb(cr, 1, 1, 1);
    rect.Fill(cr);
    cairo_set_source_rgb(cr, 0, 0, 0);
    cairo_set_line_width(cr, 2);
    rect.Line(cr);
    switch (type) {
    default:
        _ASSERT(0);
        break;
    case EGUIControlType::EXPAND:
    case EGUIControlType::COLLAPSE:
        cairo_set_line_width(cr, (control_size.x+control_size.y)/10);
        cairo_set_source_rgb(cr, 1, 0, 0);
        cairo_move_to(cr, -control_size.x*0.25, 0);
        cairo_line_to(cr, +control_size.x*0.25, 0);
        if (type == EGUIControlType::EXPAND) {
            cairo_new_sub_path(cr);
            cairo_move_to(cr, 0, -control_size.x*0.25);
            cairo_line_to(cr, 0, +control_size.x*0.25);
        }
        cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
        cairo_stroke(cr);
        break;
    case EGUIControlType::ARROW:
        XY points[] = {XY(-control_size.x*0.25, -control_size.y*0.10),
                       XY(-control_size.x*0.10, -control_size.y*0.10),
                       XY(-control_size.x*0.10, -control_size.y*0.25),
                       XY(control_size.x*0.25, 0),
                       XY(-control_size.x*0.10,  control_size.y*0.25),
                       XY(-control_size.x*0.10,  control_size.y*0.10),
                       XY(-control_size.x*0.25,  control_size.y*0.10)
        };
        Contour arrow;
        arrow.assign_dont_check(points);
        //LineAttr line(ELineType::SOLID, ColorType(0,0,0), 1, ECornerType::NONE, 0);
        //FillAttr fill(ColorType(0,128,0), EGradientType::UP);
        cairo_set_source_rgb(cr, 0.2, 0.5, 0.2);
        arrow.Fill(cr);
        cairo_set_source_rgb(cr, 0, 0, 0);
        arrow.Line(cr);
        break;
    }
    cairo_restore(cr);
}

/** Draw GUI controls (if any)
 * @param cr The cairo context to draw the controls to. 
 *           The transformation matrix shall be so that we can draw on chart space.
 * @param [in] size A number between [0.01..1] indicating a scale factor. */
void Element::DrawControls(cairo_t *cr, double size) const
{
    if (size<0.01 || size>1 || controls.size()==0 || cr==nullptr) return;
    cairo_save(cr);
    XY ul = control_location.UpperLeft();
    cairo_translate(cr, ul.x, ul.y);
    for (EGUIControlType type : controls) {
        DrawControl(cr, type, size);
        //move down to next control location
        cairo_translate(cr, 0, control_size.y);
    }
    cairo_restore(cr);
}

/** Return the type of GUI control `xy` points to.
 * @param [in] xy The coordinates are in chart space.
 * @returns The type of control or EGUIControlType::INVALID, if xy does not point to any control.*/
EGUIControlType Element::WhichControl(const XY &xy) const noexcept
{
    if (!control_location.IsWithinBool(xy)) return EGUIControlType::INVALID;
    return controls[unsigned((xy.y - control_location.y.from)/control_size.y)];
}


/** Return the outer Edge of indicators.
 * @param [in] pos The middle of the top edge of the indicator shall be here.
 * @param [in] line The line style of the indicator
 * @param [in] fill The fill style of the indicator
 * @param [in] shadow The shadow style of the indicator
 * @returns The outer edge.*/
Block Element::GetIndicatorCover(const XY& pos, const LineAttr& line, 
                                 const FillAttr&, const ShadowAttr&) {
    Block b(pos.x-indicator_size.x/2, pos.x+indicator_size.x/2,
            pos.y, pos.y+indicator_size.y);
    return b.Expand(line.LineWidth());
}

/** Draw an indicator.
 * @param [in] pos The middle of the top edge of the indicator shall be here.
 * @param canvas The canvas to draw on.
 * @param [in] line The line style of the indicator
 * @param [in] fill The fill style of the indicator
 * @param [in] shadow The shadow style of the indicator*/
void Element::DrawIndicator(XY pos, Canvas* canvas, const LineAttr &line, 
                            const FillAttr &fill, const ShadowAttr &shadow) {
    if (!canvas) return;

    const Block area = GetIndicatorCover(pos, line, fill, shadow);
    LineAttr line_dot(line);
    line_dot.width = area.y.Spans()/4;
    line_dot.type = ELineType::SOLID;
    pos.y += indicator_size.y/2;
    const XY offset(area.x.Spans()/4, 0);
    if (canvas->does_graphics()) {
        if (canvas->cannot_draw()) return;
        canvas->Shadow(area, line, shadow);
        canvas->Fill(area, line, fill);
        canvas->Line(area, line);

        cairo_save(canvas->GetContext());
        cairo_set_line_cap(canvas->GetContext(), CAIRO_LINE_CAP_ROUND);
        canvas->Line(pos,        pos,        line_dot);
        canvas->Line(pos-offset, pos-offset, line_dot);
        canvas->Line(pos+offset, pos+offset, line_dot);
        cairo_restore(canvas->GetContext());
    } else {
        canvas->Add(GSBox(area, line, fill, shadow));
        const Contour dot(pos, line.LineWidth());
        canvas->Add(GSShape(dot.CreateShifted(-offset) + dot + dot.CreateShifted(offset),
                            LineAttr::None(), FillAttr::Solid(*line.color)));
    }
}



