/*
 * Copyright (c) 2020 - 2026 ThorVG project. All rights reserved.

 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:

 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.

 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include "tvgSwCommon.h"

/************************************************************************/
/* Internal Class Implementation                                        */
/************************************************************************/

static bool _outlineBegin(SwOutline& outline)
{
    //Make a contour if lineTo/curveTo without calling close or moveTo beforehand.
    if (outline.pts.empty()) return false;
    outline.cntrs.push(outline.pts.count - 1);
    outline.closed.push(false);
    outline.pts.push(outline.pts[outline.cntrs.last()]);
    outline.types.push(SW_CURVE_TYPE_POINT);
    return false;
}


static bool _outlineEnd(SwOutline& outline)
{
    if (outline.pts.empty()) return false;
    outline.cntrs.push(outline.pts.count - 1);
    outline.closed.push(false);
    return false;
}


static bool _outlineMoveTo(SwOutline& outline, const Point* to, const Matrix& transform, bool closed = false)
{
    //make it a contour, if the last contour is not closed yet.
    if (!closed) _outlineEnd(outline);

    outline.pts.push(mathTransform(to, transform));
    outline.types.push(SW_CURVE_TYPE_POINT);
    return false;
}


static void _outlineLineTo(SwOutline& outline, const Point* to, const Matrix& transform)
{
    outline.pts.push(mathTransform(to, transform));
    outline.types.push(SW_CURVE_TYPE_POINT);
}


static void _outlineCubicTo(SwOutline& outline, const Point* ctrl1, const Point* ctrl2, const Point* to, const Matrix& transform)
{
    outline.pts.push(mathTransform(ctrl1, transform));
    outline.types.push(SW_CURVE_TYPE_CUBIC);

    outline.pts.push(mathTransform(ctrl2, transform));
    outline.types.push(SW_CURVE_TYPE_CUBIC);    

    outline.pts.push(mathTransform(to, transform));
    outline.types.push(SW_CURVE_TYPE_POINT);
}


static bool _outlineClose(SwOutline& outline)
{
    uint32_t i;
    if (outline.cntrs.count > 0) i = outline.cntrs.last() + 1;
    else i = 0;

    //Make sure there is at least one point in the current path
    if (outline.pts.count == i) return false;

    //Close the path
    outline.pts.push(outline.pts[i]);
    outline.cntrs.push(outline.pts.count - 1);
    outline.types.push(SW_CURVE_TYPE_POINT);
    outline.closed.push(true);

    return true;
}


static void _drawPoint(SwDashStroke& dash, const Point* start, const Matrix& transform)
{
    if (dash.move || dash.pattern[dash.curIdx] < FLOAT_EPSILON) {
        _outlineMoveTo(*dash.outline, start, transform);
        dash.move = false;
    }
    _outlineLineTo(*dash.outline, start, transform);
}


static void _dashLineTo(SwDashStroke& dash, const Point* to, const Matrix& transform, bool validPoint)
{
    Line cur = {dash.ptCur, *to};
    auto len = cur.length();
    if (tvg::zero(len)) {
        _outlineMoveTo(*dash.outline, &dash.ptCur, transform);
    //draw the current line fully
    } else if (len <= dash.curLen) {
        dash.curLen -= len;
        if (!dash.curOpGap) {
            if (dash.move) {
                _outlineMoveTo(*dash.outline, &dash.ptCur, transform);
                dash.move = false;
            }
            _outlineLineTo(*dash.outline, to, transform);
        }
    //draw the current line partially
    } else {
        while (len - dash.curLen > DASH_PATTERN_THRESHOLD) {
            Line left, right;
            if (dash.curLen > 0) {
                len -= dash.curLen;
                cur.split(dash.curLen, left, right);
                if (!dash.curOpGap) {
                    if (dash.move || dash.pattern[dash.curIdx] - dash.curLen < FLOAT_EPSILON) {
                        _outlineMoveTo(*dash.outline, &left.pt1, transform);
                        dash.move = false;
                    }
                    _outlineLineTo(*dash.outline, &left.pt2, transform);
                }
            } else {
                if (validPoint && !dash.curOpGap) _drawPoint(dash, &cur.pt1, transform);
                right = cur;
            }
            dash.curIdx = (dash.curIdx + 1) % dash.cnt;
            dash.curLen = dash.pattern[dash.curIdx];
            dash.curOpGap = !dash.curOpGap;
            cur = right;
            dash.ptCur = cur.pt1;
            dash.move = true;
        }
        //leftovers
        dash.curLen -= len;
        if (!dash.curOpGap) {
            if (dash.move) {
                _outlineMoveTo(*dash.outline, &cur.pt1, transform);
                dash.move = false;
            }
            _outlineLineTo(*dash.outline, &cur.pt2, transform);
        }
        if (dash.curLen < 1 && TO_SWCOORD(len) > 1) {
            //move to next dash
            dash.curIdx = (dash.curIdx + 1) % dash.cnt;
            dash.curLen = dash.pattern[dash.curIdx];
            dash.curOpGap = !dash.curOpGap;
        }
    }
    dash.ptCur = *to;
}


static void _dashCubicTo(SwDashStroke& dash, const Point* ctrl1, const Point* ctrl2, const Point* to, const Matrix& transform, bool validPoint)
{
    Bezier cur = {dash.ptCur, *ctrl1, *ctrl2, *to};
    auto len = cur.length();

    //draw the current line fully
    if (tvg::zero(len)) {
        _outlineMoveTo(*dash.outline, &dash.ptCur, transform);
    } else if (len <= dash.curLen) {
        dash.curLen -= len;
        if (!dash.curOpGap) {
            if (dash.move) {
                _outlineMoveTo(*dash.outline, &dash.ptCur, transform);
                dash.move = false;
            }
            _outlineCubicTo(*dash.outline, ctrl1, ctrl2, to, transform);
        }
    //draw the current line partially
    } else {
        while ((len - dash.curLen) > DASH_PATTERN_THRESHOLD) {
            Bezier left, right;
            if (dash.curLen > 0) {
                len -= dash.curLen;
                cur.split(dash.curLen, left, right);
                if (!dash.curOpGap) {
                    if (dash.move || dash.pattern[dash.curIdx] - dash.curLen < FLOAT_EPSILON) {
                        _outlineMoveTo(*dash.outline, &left.start, transform);
                        dash.move = false;
                    }
                    _outlineCubicTo(*dash.outline, &left.ctrl1, &left.ctrl2, &left.end, transform);
                }
            } else {
                if (validPoint && !dash.curOpGap) _drawPoint(dash, &cur.start, transform);
                right = cur;
            }
            dash.curIdx = (dash.curIdx + 1) % dash.cnt;
            dash.curLen = dash.pattern[dash.curIdx];
            dash.curOpGap = !dash.curOpGap;
            cur = right;
            dash.ptCur = right.start;
            dash.move = true;
        }
        //leftovers
        dash.curLen -= len;
        if (!dash.curOpGap) {
            if (dash.move) {
                _outlineMoveTo(*dash.outline, &cur.start, transform);
                dash.move = false;
            }
            _outlineCubicTo(*dash.outline, &cur.ctrl1, &cur.ctrl2, &cur.end, transform);
        }
        if (dash.curLen < 0.1f && TO_SWCOORD(len) > 1) {
            //move to next dash
            dash.curIdx = (dash.curIdx + 1) % dash.cnt;
            dash.curLen = dash.pattern[dash.curIdx];
            dash.curOpGap = !dash.curOpGap;
        }
    }
    dash.ptCur = *to;
}


static void _dashClose(SwDashStroke& dash, const Matrix& transform, bool validPoint)
{
    _dashLineTo(dash, &dash.ptStart, transform, validPoint);
}


static void _dashMoveTo(SwDashStroke& dash, uint32_t offIdx, float offset, const Point* pts)
{
    dash.curIdx = offIdx % dash.cnt;
    dash.curLen = dash.pattern[dash.curIdx] - offset;
    dash.curOpGap = offIdx % 2;
    dash.ptStart = dash.ptCur = *pts;
    dash.move = true;
}


static SwOutline* _genDashOutline(const RenderShape* rshape, const Matrix& transform, SwMpool* mpool, unsigned tid, bool trimmed)
{
    PathCommand *cmds, *trimmedCmds = nullptr;
    Point *pts, *trimmedPts = nullptr;
    uint32_t cmdCnt, ptsCnt;

    if (trimmed) {
        RenderPath trimmedPath;
        if (!rshape->stroke->trim.trim(rshape->path, trimmedPath)) return nullptr;
        cmds = trimmedCmds = trimmedPath.cmds.data;
        cmdCnt = trimmedPath.cmds.count;
        pts = trimmedPts = trimmedPath.pts.data;
        ptsCnt = trimmedPath.pts.count;

        trimmedPath.cmds.data = nullptr;
        trimmedPath.pts.data = nullptr;
    } else {
        cmds = rshape->path.cmds.data;
        cmdCnt = rshape->path.cmds.count;
        pts = rshape->path.pts.data;
        ptsCnt = rshape->path.pts.count;
    }

    //No actual shape data
    if (cmdCnt == 0 || ptsCnt == 0) return nullptr;

    SwDashStroke dash;
    dash.pattern = rshape->stroke->dash.pattern;
    dash.cnt = rshape->stroke->dash.count;
    auto offset = rshape->stroke->dash.offset;

    //offset
    uint32_t offIdx = 0;
    if (!tvg::zero(offset)) {
        auto length = rshape->stroke->dash.length;
        bool isOdd = dash.cnt % 2;
        if (isOdd) length *= 2;

        offset = fmodf(offset, length);
        if (offset < 0) offset += length;

        for (size_t i = 0; i < dash.cnt * (1 + (size_t)isOdd); ++i, ++offIdx) {
            auto curPattern = dash.pattern[i % dash.cnt];
            if (offset < curPattern) break;
            offset -= curPattern;
        }
    }

    dash.outline = mpool->outline(tid);

    //must begin with moveTo
    if (cmds[0] == PathCommand::MoveTo) {
        _dashMoveTo(dash, offIdx, offset, pts);
        cmds++;
        pts++;
    }

    //zero length segment with non-butt cap still should be rendered as a point - only the caps are visible
    auto validPoint = rshape->stroke->cap != StrokeCap::Butt;
    while (--cmdCnt > 0) {
        switch (*cmds) {
            case PathCommand::Close: {
                _dashClose(dash, transform, validPoint);
                break;
            }
            case PathCommand::MoveTo: {
                _dashMoveTo(dash, offIdx, offset, pts);
                ++pts;
                break;
            }
            case PathCommand::LineTo: {
                _dashLineTo(dash, pts, transform, validPoint);
                ++pts;
                break;
            }
            case PathCommand::CubicTo: {
                _dashCubicTo(dash, pts, pts + 1, pts + 2, transform, validPoint);
                pts += 3;
                break;
            }
        }
        ++cmds;
    }

    _outlineEnd(*dash.outline);

    dash.outline->fillRule = rshape->rule;

    tvg::free(trimmedCmds);
    tvg::free(trimmedPts);

    return dash.outline;
}


static bool _axisAlignedRect(const SwOutline* outline)
{
    //Fast Track: axis-aligned rectangle?
    if (outline->pts.count != 5) return false;
    if (outline->types[2] == SW_CURVE_TYPE_CUBIC) return false;

    auto pt1 = outline->pts.data + 0;
    auto pt2 = outline->pts.data + 1;
    auto pt3 = outline->pts.data + 2;
    auto pt4 = outline->pts.data + 3;

    auto a = SwPoint{pt1->x, pt3->y};
    auto b = SwPoint{pt3->x, pt1->y};

    if ((*pt2 == a && *pt4 == b) || (*pt2 == b && *pt4 == a)) return true;

    return false;
}


static SwOutline* _genOutline(SwShape& shape, const RenderShape* rshape, const Matrix& transform, SwMpool* mpool, unsigned tid, bool hasComposite, bool trimmed = false)
{
    PathCommand *cmds, *trimmedCmds = nullptr;
    Point *pts, *trimmedPts = nullptr;
    uint32_t cmdCnt, ptsCnt;

    if (trimmed) {
        RenderPath trimmedPath;
        if (!rshape->stroke->trim.trim(rshape->path, trimmedPath)) return nullptr;
        cmds = trimmedCmds = trimmedPath.cmds.data;
        cmdCnt = trimmedPath.cmds.count;
        pts = trimmedPts = trimmedPath.pts.data;
        ptsCnt = trimmedPath.pts.count;

        trimmedPath.cmds.data = nullptr;
        trimmedPath.pts.data = nullptr;
    } else {
        cmds = rshape->path.cmds.data;
        cmdCnt = rshape->path.cmds.count;
        pts = rshape->path.pts.data;
        ptsCnt = rshape->path.pts.count;
    }

    //No actual shape data
    if (cmdCnt == 0 || ptsCnt == 0) return nullptr;

    auto outline = mpool->outline(tid);
    auto closed = false;

    //Generate Outlines
    while (cmdCnt-- > 0) {
        switch (*cmds) {
            case PathCommand::Close: {
                if (!closed) closed = _outlineClose(*outline);
                break;
            }
            case PathCommand::MoveTo: {
                closed = _outlineMoveTo(*outline, pts, transform, closed);
                ++pts;
                break;
            }
            case PathCommand::LineTo: {
                if (closed) closed = _outlineBegin(*outline);
                _outlineLineTo(*outline, pts, transform);
                ++pts;
                break;
            }
            case PathCommand::CubicTo: {
                if (closed) closed = _outlineBegin(*outline);
                _outlineCubicTo(*outline, pts, pts + 1, pts + 2, transform);
                pts += 3;
                break;
            }
        }
        ++cmds;
    }

    if (!closed) _outlineEnd(*outline);

    outline->fillRule = rshape->rule;

    tvg::free(trimmedCmds);
    tvg::free(trimmedPts);

    shape.fastTrack = (!hasComposite && _axisAlignedRect(outline));
    return outline;
}


/************************************************************************/
/* External Class Implementation                                        */
/************************************************************************/

bool shapePrepare(SwShape& shape, const RenderShape* rshape, const Matrix& transform, const RenderRegion& clipBox, RenderRegion& renderBox, SwMpool* mpool, unsigned tid, bool hasComposite)
{
    if ((shape.outline = _genOutline(shape, rshape, transform, mpool, tid, hasComposite, rshape->trimpath()))) {
        if (mathUpdateOutlineBBox(shape.outline, clipBox, renderBox, shape.fastTrack)) {
            shape.bbox = renderBox;
            return true;
        }
    }
    return false;
}


bool shapeGenRle(SwShape& shape, const RenderRegion& bbox, SwMpool* mpool, unsigned tid, bool antiAlias)
{
    //Case A: Fast Track Rectangle Drawing
    if (shape.fastTrack) return true;

    //Case B: Normal Shape RLE Drawing
    if ((shape.rle = rleRender(shape.rle, shape.outline, bbox, mpool, tid, antiAlias))) return true;

    return false;
}


void shapeDelOutline(SwShape& shape, SwMpool* mpool, uint32_t tid)
{
    shape.outline = nullptr;
}


void shapeReset(SwShape& shape)
{
    rleReset(shape.rle);
    shape.bbox.reset();
    shape.fastTrack = false;
}


void shapeFree(SwShape& shape)
{
    rleFree(shape.rle);
    shape.rle = nullptr;

    shapeDelFill(shape);

    if (shape.stroke) {
        rleFree(shape.strokeRle);
        shape.strokeRle = nullptr;
        strokeFree(shape.stroke);
        shape.stroke = nullptr;
    }
}


void shapeDelStroke(SwShape& shape)
{
    if (!shape.stroke) return;
    rleFree(shape.strokeRle);
    shape.strokeRle = nullptr;
    strokeFree(shape.stroke);
    shape.stroke = nullptr;
}


void shapeResetStroke(SwShape& shape, const RenderShape* rshape, const Matrix& transform, SwMpool* mpool, unsigned tid)
{
    if (!shape.stroke) shape.stroke = tvg::calloc<SwStroke>(1, sizeof(SwStroke));
    auto stroke = shape.stroke;
    strokeReset(stroke, rshape, transform, mpool, tid);
    rleReset(shape.strokeRle);
}


bool shapeGenStrokeRle(SwShape& shape, const RenderShape* rshape, const Matrix& transform, const RenderRegion& clipBox, RenderRegion& renderBox, SwMpool* mpool, unsigned tid)
{
    SwOutline* shapeOutline = nullptr;

    //Dash style with/without trimming
    if (rshape->stroke->dash.length > DASH_PATTERN_THRESHOLD) {
        shapeOutline = _genDashOutline(rshape, transform, mpool, tid, rshape->trimpath());
    //Trimming & Normal style
    } else {
        shapeOutline = shape.outline ? shape.outline : _genOutline(shape, rshape, transform, mpool, tid, false, rshape->trimpath());
    }

    if (!shapeOutline) return false;

    if (!strokeParseOutline(shape.stroke, *shapeOutline, mpool, tid)) return false;

    auto strokeOutline = strokeExportOutline(shape.stroke, mpool, tid);
    auto ret = mathUpdateOutlineBBox(strokeOutline, clipBox, renderBox, false);
    if (ret) shape.strokeRle = rleRender(shape.strokeRle, strokeOutline, renderBox, mpool, tid, true);

    return ret;
}


bool shapeGenFillColors(SwShape& shape, const Fill* fill, const Matrix& transform, SwSurface* surface, uint8_t opacity, bool ctable)
{
    return fillGenColorTable(shape.fill, fill, transform, surface, opacity, ctable);
}


bool shapeGenStrokeFillColors(SwShape& shape, const Fill* fill, const Matrix& transform, SwSurface* surface, uint8_t opacity, bool ctable)
{
    return fillGenColorTable(shape.stroke->fill, fill, transform, surface, opacity, ctable);
}


void shapeResetFill(SwShape& shape)
{
    if (!shape.fill) {
        shape.fill = tvg::calloc<SwFill>(1, sizeof(SwFill));
        if (!shape.fill) return;
    }
    fillReset(shape.fill);
}


void shapeResetStrokeFill(SwShape& shape)
{
    if (!shape.stroke->fill) {
        shape.stroke->fill = tvg::calloc<SwFill>(1, sizeof(SwFill));
        if (!shape.stroke->fill) return;
    }
    fillReset(shape.stroke->fill);
}


void shapeDelFill(SwShape& shape)
{
    if (!shape.fill) return;
    fillFree(shape.fill);
    shape.fill = nullptr;
}


bool shapeStrokeBBox(SwShape& shape, const RenderShape* rshape, Point* pt4, const Matrix& m, SwMpool* mpool)
{
    auto outline = _genOutline(shape, rshape, m, mpool, 0, false, rshape->trimpath());
    if (!outline) return false;

    if (rshape->strokeWidth() > 0.0f) {
        strokeReset(shape.stroke, rshape, m, mpool, 0);
        strokeParseOutline(shape.stroke, *outline, mpool, 0);

        auto func = [](SwStrokeBorder* border, SwPoint& min, SwPoint& max) {
            ARRAY_FOREACH(pts, border->pts) {
                if (pts->x < min.x) min.x = pts->x;
                if (pts->x > max.x) max.x = pts->x;
                if (pts->y < min.y) min.y = pts->y;
                if (pts->y > max.y) max.y = pts->y;
            }
        };

        SwPoint min = {INT32_MAX, INT32_MAX};
        SwPoint max = {-INT32_MAX, -INT32_MAX};
        func(shape.stroke->borders[0], min, max);
        func(shape.stroke->borders[1], min, max);

        pt4[0] = min.toPoint();
        pt4[1] = SwPoint{max.x, min.y}.toPoint();
        pt4[2] = max.toPoint();
        pt4[3] = SwPoint{min.x, max.y}.toPoint();
    }

    shapeDelOutline(shape, mpool, 0);

    return true;
}