/**
 * @file lv_draw_line.c
 *
 */

/*********************
 *      INCLUDES
 *********************/
/* #include <stdbool.h> */
#include "generic/typedef.h"
#include "lv_draw_mask.h"
#include "lv_draw_blend.h"
#include "../core/lv_refr.h"
#include "../misc/lv_math.h"

/*********************
 *      DEFINES
 *********************/

/**********************
 *      TYPEDEFS
 **********************/

/**********************
 *  STATIC PROTOTYPES
 **********************/

LV_ATTRIBUTE_FAST_MEM static void draw_line_skew(const lv_point_t *point1, const lv_point_t *point2,
        const lv_area_t *clip,
        const lv_draw_line_dsc_t *dsc);

LV_ATTRIBUTE_FAST_MEM static void draw_line_hor(const lv_point_t *point1, const lv_point_t *point2,
        const lv_area_t *clip,
        const lv_draw_line_dsc_t *dsc);
LV_ATTRIBUTE_FAST_MEM static void draw_line_ver(const lv_point_t *point1, const lv_point_t *point2,
        const lv_area_t *clip,
        const lv_draw_line_dsc_t *dsc);
/**********************
 *  STATIC VARIABLES
 **********************/

/**********************
 *      MACROS
 **********************/

/**********************
 *   GLOBAL FUNCTIONS
 **********************/

LV_ATTRIBUTE_FAST_MEM void lv_draw_line_dsc_init(lv_draw_line_dsc_t *dsc)
{
    lv_memset_00(dsc, sizeof(lv_draw_line_dsc_t));
    dsc->width = 1;
    dsc->opa = LV_OPA_COVER;
    dsc->color = lv_color_black();
}

/**
 * Draw a line
 * @param point1 first point of the line
 * @param point2 second point of the line
 * @param clip the line will be drawn only in this area
 * @param dsc pointer to an initialized `lv_draw_line_dsc_t` variable
 */
LV_ATTRIBUTE_FAST_MEM void lv_draw_line(const lv_point_t *point1, const lv_point_t *point2, const lv_area_t *clip,
                                        const lv_draw_line_dsc_t *dsc)
{
    if (dsc->width == 0) {
        return;
    }
    if (dsc->opa <= LV_OPA_MIN) {
        return;
    }

    if (point1->x == point2->x && point1->y == point2->y) {
        return;
    }

    lv_area_t clip_line;
    clip_line.x1 = LV_MIN(point1->x, point2->x) - dsc->width / 2;
    clip_line.x2 = LV_MAX(point1->x, point2->x) + dsc->width / 2;
    clip_line.y1 = LV_MIN(point1->y, point2->y) - dsc->width / 2;
    clip_line.y2 = LV_MAX(point1->y, point2->y) + dsc->width / 2;

    bool is_common;
    is_common = _lv_area_intersect(&clip_line, &clip_line, clip);
    if (!is_common) {
        return;
    }

    if (point1->y == point2->y) {
        draw_line_hor(point1, point2, &clip_line, dsc);
    } else if (point1->x == point2->x) {
        draw_line_ver(point1, point2, &clip_line, dsc);
    } else {
        draw_line_skew(point1, point2, &clip_line, dsc);
    }

    if (dsc->round_end || dsc->round_start) {
        lv_draw_rect_dsc_t cir_dsc;
        lv_draw_rect_dsc_init(&cir_dsc);
        cir_dsc.bg_color = dsc->color;
        cir_dsc.radius = LV_RADIUS_CIRCLE;
        cir_dsc.bg_opa = dsc->opa;

        int32_t r = (dsc->width >> 1);
        int32_t r_corr = (dsc->width & 1) ? 0 : 1;
        lv_area_t cir_area;

        if (dsc->round_start) {
            cir_area.x1 = point1->x - r;
            cir_area.y1 = point1->y - r;
            cir_area.x2 = point1->x + r - r_corr;
            cir_area.y2 = point1->y + r - r_corr ;
            lv_draw_rect(&cir_area, clip, &cir_dsc);
        }

        if (dsc->round_end) {
            cir_area.x1 = point2->x - r;
            cir_area.y1 = point2->y - r;
            cir_area.x2 = point2->x + r - r_corr;
            cir_area.y2 = point2->y + r - r_corr ;
            lv_draw_rect(&cir_area, clip, &cir_dsc);
        }
    }
}

/**********************
 *   STATIC FUNCTIONS
 **********************/


LV_ATTRIBUTE_FAST_MEM static void draw_line_hor(const lv_point_t *point1, const lv_point_t *point2,
        const lv_area_t *clip,
        const lv_draw_line_dsc_t *dsc)
{
    lv_opa_t opa = dsc->opa;

    int32_t w = dsc->width - 1;
    int32_t w_half0 = w >> 1;
    int32_t w_half1 = w_half0 + (w & 0x1); /*Compensate rounding error*/



    lv_area_t draw_area;
    draw_area.x1 = LV_MIN(point1->x, point2->x);
    draw_area.x2 = LV_MAX(point1->x, point2->x)  - 1;
    draw_area.y1 = point1->y - w_half1;
    draw_area.y2 = point1->y + w_half0;

    bool dashed = dsc->dash_gap && dsc->dash_width ? true : false;
    bool simple_mode = true;
    if (lv_draw_mask_is_any(&draw_area)) {
        simple_mode = false;
    } else if (dashed) {
        simple_mode = false;
    }

    /*If there is no mask then simply draw a rectangle*/
    if (simple_mode) {
        _lv_blend_fill(clip, &draw_area,
                       dsc->color, NULL, LV_DRAW_MASK_RES_FULL_COVER, opa,
                       dsc->blend_mode);
    }
#if LV_DRAW_COMPLEX
    /*If there other mask apply it*/
    else {
        lv_disp_t *disp = _lv_refr_get_disp_refreshing();
        lv_disp_draw_buf_t *draw_buf = lv_disp_get_draw_buf(disp);
        const lv_area_t *disp_area = &draw_buf->area;
        /*Get clipped fill area which is the real draw area.
         *It is always the same or inside `fill_area`*/
        bool is_common;
        is_common = _lv_area_intersect(&draw_area, clip, &draw_area);
        if (!is_common) {
            return;
        }

        /*Now `draw_area` has absolute coordinates.
         *Make it relative to `disp_area` to simplify draw to `disp_buf`*/
        draw_area.x1 -= disp_area->x1;
        draw_area.y1 -= disp_area->y1;
        draw_area.x2 -= disp_area->x1;
        draw_area.y2 -= disp_area->y1;

        int32_t draw_area_w = lv_area_get_width(&draw_area);

        lv_area_t fill_area;
        fill_area.x1 = draw_area.x1 + disp_area->x1;
        fill_area.x2 = draw_area.x2 + disp_area->x1;
        fill_area.y1 = draw_area.y1 + disp_area->y1;
        fill_area.y2 = fill_area.y1;

        lv_coord_t dash_start = 0;
        if (dashed) {
            dash_start = (draw_buf->area.x1 + draw_area.x1) % (dsc->dash_gap + dsc->dash_width);
        }

        lv_opa_t *mask_buf = lv_mem_buf_get(draw_area_w);
        int32_t h;
        for (h = draw_area.y1; h <= draw_area.y2; h++) {
            lv_memset_ff(mask_buf, draw_area_w);
            lv_draw_mask_res_t mask_res = lv_draw_mask_apply(mask_buf, draw_buf->area.x1 + draw_area.x1, draw_buf->area.y1 + h,
                                          draw_area_w);

            if (dashed) {
                if (mask_res != LV_DRAW_MASK_RES_TRANSP) {
                    lv_coord_t dash_cnt = dash_start;
                    lv_coord_t i;
                    for (i = 0; i < draw_area_w; i++, dash_cnt++) {
                        if (dash_cnt <= dsc->dash_width) {
                            int16_t diff = dsc->dash_width - dash_cnt;
                            i += diff;
                            dash_cnt += diff;
                        } else if (dash_cnt >= dsc->dash_gap + dsc->dash_width) {
                            dash_cnt = 0;
                        } else {
                            mask_buf[i] = 0x00;
                        }
                    }

                    mask_res = LV_DRAW_MASK_RES_CHANGED;
                }
            }

            _lv_blend_fill(clip, &fill_area,
                           dsc->color, mask_buf, mask_res, dsc->opa,
                           dsc->blend_mode);

            fill_area.y1++;
            fill_area.y2++;
        }
        lv_mem_buf_release(mask_buf);
    }
#endif /*LV_DRAW_COMPLEX*/
}

LV_ATTRIBUTE_FAST_MEM static void draw_line_ver(const lv_point_t *point1, const lv_point_t *point2,
        const lv_area_t *clip,
        const lv_draw_line_dsc_t *dsc)
{
    lv_opa_t opa = dsc->opa;

    int32_t w = dsc->width - 1;
    int32_t w_half0 = w >> 1;
    int32_t w_half1 = w_half0 + (w & 0x1); /*Compensate rounding error*/

    lv_area_t draw_area;
    draw_area.x1 = point1->x - w_half1;
    draw_area.x2 = point1->x + w_half0;
    draw_area.y1 = LV_MIN(point1->y, point2->y);
    draw_area.y2 = LV_MAX(point1->y, point2->y) - 1;

    bool dashed = dsc->dash_gap && dsc->dash_width ? true : false;
    bool simple_mode = true;
    if (lv_draw_mask_is_any(&draw_area)) {
        simple_mode = false;
    } else if (dashed) {
        simple_mode = false;
    }

    /*If there is no mask then simply draw a rectangle*/
    if (simple_mode) {
        _lv_blend_fill(clip, &draw_area,
                       dsc->color,  NULL, LV_DRAW_MASK_RES_FULL_COVER, opa,
                       dsc->blend_mode);
    }

#if LV_DRAW_COMPLEX
    /*If there other mask apply it*/
    else {
        lv_disp_t *disp = _lv_refr_get_disp_refreshing();
        lv_disp_draw_buf_t *draw_buf = lv_disp_get_draw_buf(disp);
        const lv_area_t *disp_area = &draw_buf->area;
        /*Get clipped fill area which is the real draw area.
         *It is always the same or inside `fill_area`*/
        bool is_common;
        is_common = _lv_area_intersect(&draw_area, clip, &draw_area);
        if (!is_common) {
            return;
        }

        /*Now `draw_area` has absolute coordinates.
         *Make it relative to `disp_area` to simplify draw to `disp_buf`*/
        draw_area.x1 -= draw_buf->area.x1;
        draw_area.y1 -= draw_buf->area.y1;
        draw_area.x2 -= draw_buf->area.x1;
        draw_area.y2 -= draw_buf->area.y1;

        int32_t draw_area_w = lv_area_get_width(&draw_area);

        lv_area_t fill_area;
        fill_area.x1 = draw_area.x1 + disp_area->x1;
        fill_area.x2 = draw_area.x2 + disp_area->x1;
        fill_area.y1 = draw_area.y1 + disp_area->y1;
        fill_area.y2 = fill_area.y1;

        lv_opa_t *mask_buf = lv_mem_buf_get(draw_area_w);

        lv_coord_t dash_start = 0;
        if (dashed) {
            dash_start = (draw_buf->area.y1 + draw_area.y1) % (dsc->dash_gap + dsc->dash_width);
        }

        lv_coord_t dash_cnt = dash_start;

        int32_t h;
        for (h = draw_area.y1; h <= draw_area.y2; h++) {
            lv_memset_ff(mask_buf, draw_area_w);
            lv_draw_mask_res_t mask_res = lv_draw_mask_apply(mask_buf, draw_buf->area.x1 + draw_area.x1, draw_buf->area.y1 + h,
                                          draw_area_w);

            if (dashed) {
                if (mask_res != LV_DRAW_MASK_RES_TRANSP) {
                    if (dash_cnt > dsc->dash_width) {
                        mask_res = LV_DRAW_MASK_RES_TRANSP;
                    }

                    if (dash_cnt >= dsc->dash_gap + dsc->dash_width) {
                        dash_cnt = 0;
                    }
                }
                dash_cnt ++;
            }

            _lv_blend_fill(clip, &fill_area,
                           dsc->color, mask_buf, mask_res, dsc->opa,
                           LV_BLEND_MODE_NORMAL);

            fill_area.y1++;
            fill_area.y2++;
        }
        lv_mem_buf_release(mask_buf);
    }
#endif /*LV_DRAW_COMPLEX*/
}

LV_ATTRIBUTE_FAST_MEM static void draw_line_skew(const lv_point_t *point1, const lv_point_t *point2,
        const lv_area_t *clip,
        const lv_draw_line_dsc_t *dsc)
{
#if LV_DRAW_COMPLEX
    /*Keep the great y in p1*/
    lv_point_t p1;
    lv_point_t p2;
    if (point1->y < point2->y) {
        p1.y = point1->y;
        p2.y = point2->y;
        p1.x = point1->x;
        p2.x = point2->x;
    } else {
        p1.y = point2->y;
        p2.y = point1->y;
        p1.x = point2->x;
        p2.x = point1->x;
    }

    int32_t xdiff = p2.x - p1.x;
    int32_t ydiff = p2.y - p1.y;
    bool flat = LV_ABS(xdiff) > LV_ABS(ydiff) ? true : false;

    static const uint8_t wcorr[] = {
        128, 128, 128, 129, 129, 130, 130, 131,
        132, 133, 134, 135, 137, 138, 140, 141,
        143, 145, 147, 149, 151, 153, 155, 158,
        160, 162, 165, 167, 170, 173, 175, 178,
        181,
    };

    int32_t w = dsc->width;
    int32_t wcorr_i = 0;
    if (flat) {
        wcorr_i = (LV_ABS(ydiff) << 5) / LV_ABS(xdiff);
    } else {
        wcorr_i = (LV_ABS(xdiff) << 5) / LV_ABS(ydiff);
    }

    w = (w * wcorr[wcorr_i] + 63) >> 7;     /*+ 63 for rounding*/
    int32_t w_half0 = w >> 1;
    int32_t w_half1 = w_half0 + (w & 0x1); /*Compensate rounding error*/

    lv_area_t draw_area;
    draw_area.x1 = LV_MIN(p1.x, p2.x) - w;
    draw_area.x2 = LV_MAX(p1.x, p2.x) + w;
    draw_area.y1 = LV_MIN(p1.y, p2.y) - w;
    draw_area.y2 = LV_MAX(p1.y, p2.y) + w;

    /*Get the union of `coords` and `clip`*/
    /*`clip` is already truncated to the `draw_buf` size
     *in 'lv_refr_area' function*/
    bool is_common = _lv_area_intersect(&draw_area, &draw_area, clip);
    if (is_common == false) {
        return;
    }

    lv_draw_mask_line_param_t mask_left_param;
    lv_draw_mask_line_param_t mask_right_param;
    lv_draw_mask_line_param_t mask_top_param;
    lv_draw_mask_line_param_t mask_bottom_param;

    if (flat) {
        if (xdiff > 0) {
            lv_draw_mask_line_points_init(&mask_left_param, p1.x, p1.y - w_half0, p2.x, p2.y - w_half0,
                                          LV_DRAW_MASK_LINE_SIDE_LEFT);
            lv_draw_mask_line_points_init(&mask_right_param, p1.x, p1.y + w_half1, p2.x, p2.y + w_half1,
                                          LV_DRAW_MASK_LINE_SIDE_RIGHT);
        } else {
            lv_draw_mask_line_points_init(&mask_left_param, p1.x, p1.y + w_half1, p2.x, p2.y + w_half1,
                                          LV_DRAW_MASK_LINE_SIDE_LEFT);
            lv_draw_mask_line_points_init(&mask_right_param, p1.x, p1.y - w_half0, p2.x, p2.y - w_half0,
                                          LV_DRAW_MASK_LINE_SIDE_RIGHT);
        }
    } else {
        lv_draw_mask_line_points_init(&mask_left_param, p1.x + w_half1, p1.y, p2.x + w_half1, p2.y,
                                      LV_DRAW_MASK_LINE_SIDE_LEFT);
        lv_draw_mask_line_points_init(&mask_right_param, p1.x - w_half0, p1.y, p2.x - w_half0, p2.y,
                                      LV_DRAW_MASK_LINE_SIDE_RIGHT);
    }

    /*Use the normal vector for the endings*/

    int16_t mask_left_id = lv_draw_mask_add(&mask_left_param, NULL);
    int16_t mask_right_id = lv_draw_mask_add(&mask_right_param, NULL);
    int16_t mask_top_id = LV_MASK_ID_INV;
    int16_t mask_bottom_id = LV_MASK_ID_INV;

    if (!dsc->raw_end) {
        lv_draw_mask_line_points_init(&mask_top_param, p1.x, p1.y, p1.x - ydiff, p1.y + xdiff, LV_DRAW_MASK_LINE_SIDE_BOTTOM);
        lv_draw_mask_line_points_init(&mask_bottom_param, p2.x, p2.y, p2.x - ydiff, p2.y + xdiff,  LV_DRAW_MASK_LINE_SIDE_TOP);
        mask_top_id = lv_draw_mask_add(&mask_top_param, NULL);
        mask_bottom_id = lv_draw_mask_add(&mask_bottom_param, NULL);
    }

    lv_disp_t *disp = _lv_refr_get_disp_refreshing();
    lv_disp_draw_buf_t *draw_buf = lv_disp_get_draw_buf(disp);

    const lv_area_t *disp_area = &draw_buf->area;

    /*Store the coordinates of the `draw_a` relative to the draw_buf*/
    draw_area.x1 -= disp_area->x1;
    draw_area.y1 -= disp_area->y1;
    draw_area.x2 -= disp_area->x1;
    draw_area.y2 -= disp_area->y1;

    /*The real draw area is around the line.
     *It's easy to calculate with steep lines, but the area can be very wide with very flat lines.
     *So deal with it only with steep lines.*/
    int32_t draw_area_w = lv_area_get_width(&draw_area);

    /*Draw the background line by line*/
    int32_t h;
    uint32_t hor_res = (uint32_t)lv_disp_get_hor_res(disp);
    size_t mask_buf_size = LV_MIN(lv_area_get_size(&draw_area), hor_res);
    lv_opa_t *mask_buf = lv_mem_buf_get(mask_buf_size);

    lv_area_t fill_area;
    fill_area.x1 = draw_area.x1 + disp_area->x1;
    fill_area.x2 = draw_area.x2 + disp_area->x1;
    fill_area.y1 = draw_area.y1 + disp_area->y1;
    fill_area.y2 = fill_area.y1;

    int32_t x = draw_buf->area.x1 + draw_area.x1;

    uint32_t mask_p = 0;

    lv_memset_ff(mask_buf, mask_buf_size);
    /*Fill the first row with 'color'*/
    for (h = draw_area.y1 + disp_area->y1; h <= draw_area.y2 + disp_area->y1; h++) {

        lv_draw_mask_res_t mask_res = lv_draw_mask_apply(&mask_buf[mask_p], x, h, draw_area_w);
        if (mask_res == LV_DRAW_MASK_RES_TRANSP) {
            lv_memset_00(&mask_buf[mask_p], draw_area_w);
        }

        mask_p += draw_area_w;
        if ((uint32_t) mask_p + draw_area_w < mask_buf_size) {
            fill_area.y2 ++;
        } else {
            _lv_blend_fill(&fill_area, clip,
                           dsc->color, mask_buf, LV_DRAW_MASK_RES_CHANGED, dsc->opa,
                           dsc->blend_mode);

            fill_area.y1 = fill_area.y2 + 1;
            fill_area.y2 = fill_area.y1;
            mask_p = 0;
            lv_memset_ff(mask_buf, mask_buf_size);
        }
    }

    /*Flush the last part*/
    if (fill_area.y1 != fill_area.y2) {
        fill_area.y2--;
        _lv_blend_fill(&fill_area, clip,
                       dsc->color, mask_buf, LV_DRAW_MASK_RES_CHANGED, dsc->opa,
                       dsc->blend_mode);

    }

    lv_mem_buf_release(mask_buf);

    lv_draw_mask_free_param(&mask_left_param);
    lv_draw_mask_free_param(&mask_right_param);
    if (mask_top_id != LV_MASK_ID_INV) {
        lv_draw_mask_free_param(&mask_top_param);
    }
    if (mask_bottom_id != LV_MASK_ID_INV) {
        lv_draw_mask_free_param(&mask_bottom_param);
    }
    lv_draw_mask_remove_id(mask_left_id);
    lv_draw_mask_remove_id(mask_right_id);
    lv_draw_mask_remove_id(mask_top_id);
    lv_draw_mask_remove_id(mask_bottom_id);
#else
    LV_UNUSED(point1);
    LV_UNUSED(point2);
    LV_UNUSED(clip);
    LV_UNUSED(dsc);
    LV_LOG_WARN("Can't draw skewed line with LV_DRAW_COMPLEX == 0");
#endif /*LV_DRAW_COMPLEX*/
}

