import {
    Container,
    Geometry,
    Mesh,
    MeshMaterial,
    MIPMAP_MODES,
    Point,
    Program,
    Texture,
    WRAP_MODES,
} from 'pixi.js';

class ScrollingMeshMaterial extends MeshMaterial
{
    constructor(texture: Texture)
    {
        const vertexSrc = `
            precision mediump float;
            attribute vec2 aVertexPosition;
            attribute vec2 aTextureCoord;
            uniform mat3 translationMatrix;
            uniform mat3 projectionMatrix;
            varying vec2 vTextureCoord;
            void main(void) {
                gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0);
                vTextureCoord = aTextureCoord;
            }
        `;

        const fragmentSrc = `
            precision mediump float;
            varying vec2 vTextureCoord;
            uniform sampler2D uSampler;
            uniform float uTextureOffset;
            void main(void) {
                vec2 uv = vec2(vTextureCoord.x + uTextureOffset, vTextureCoord.y);
                gl_FragColor = texture2D(uSampler, uv);
            }
        `;

        const program = Program.from(vertexSrc, fragmentSrc);

        super(texture, {
            program,
            uniforms: { uTextureOffset: 0 },
        });

        // Важно: разрешить повтор, иначе при смещении по U появятся швы
        if (texture?.baseTexture)
        {
            texture.baseTexture.wrapMode = WRAP_MODES.REPEAT;
            texture.baseTexture.mipmap = MIPMAP_MODES.ON;
        }
    }
}

/** Extend this class and override getters: texture, filletRadius, lineThickness, duration. */
export default class WebGLWinLine extends Container
{
    private readonly mesh: Mesh;
    private time: number = 0;
    public state: 'stopped' | 'animating' = 'stopped';

    get texture(): Texture
    {
        return Texture.from('textures/win_line.png');
    }

    get filletRadius()
    {
        return 30;
    }

    get lineThickness()
    {
        return 30;
    }

    /** duration in seconds */
    get duration()
    {
        return 1;
    }

    constructor(points: number[][], slotWidth: number = 100, slotHeight: number = 100)
    {
        super();

        const centers = points.map(([reel, row]) =>
            new Point((reel * slotWidth) + (slotWidth / 2), (row * slotHeight) + (slotHeight / 2))
        );

        const centerCurveRaw = this.computeFilletCurve(centers, this.filletRadius);
        const centerCurve = dedupeAndColinearCull(centerCurveRaw);

        this.mesh = this.createThickLineMesh(centerCurve, this.lineThickness, this.texture);
        this.addChild(this.mesh);
    }

    private computeFilletCurve(points: Point[], radius: number): Point[]
    {
        // Без филета/на острых углах важно не плодить нулевые сегменты
        const result: Point[] = [];

        if (points.length < 2) return [...points];
        if (points.length === 2) return [points[0], points[1]];
        const sampleArc = (center: Point, startAngle: number, endAngle: number, segments: number): Point[] =>
        {
            const arcPoints: Point[] = [];

            segments = Math.max(2, segments);
            const delta = (endAngle - startAngle) / (segments - 1);

            for (let i = 0; i < segments; i++)
            {
                const a = startAngle + (delta * i);

                arcPoints.push(new Point(center.x + (radius * Math.cos(a)), center.y + (radius * Math.sin(a))));
            }

            return arcPoints;
        };

        let prevT2: Point | null = null;

        // --- первый узел ---
        {
            const [p0, p1, p2] = [points[0], points[1], points[2]];
            const d1 = normalizeVector(p0.x - p1.x, p0.y - p1.y);
            const d2 = normalizeVector(p2.x - p1.x, p2.y - p1.y);
            const dot = clamp((d1[0] * d2[0]) + (d1[1] * d2[1]), -1, 1);
            const theta = Math.acos(dot);

            // если угол ≈ 0 (прямая), не создаём псевдо-дугу
            if (theta < 1e-4)
            {
                result.push(p0, p1);
                prevT2 = p1;
            }
            else
            {
                const offset = radius / Math.tan(theta / 2);
                const T1 = new Point(p1.x + (d1[0] * offset), p1.y + (d1[1] * offset));
                const T2 = new Point(p1.x + (d2[0] * offset), p1.y + (d2[1] * offset));

                result.push(p0, T1);

                const bisector = new Point(d1[0] + d2[0], d1[1] + d2[1]);
                const bisectorLen = Math.hypot(bisector.x, bisector.y);

                if (bisectorLen > 0)
                {
                    const bisectorNorm = new Point(bisector.x / bisectorLen, bisector.y / bisectorLen);
                    const centerDistance = radius / Math.sin(theta / 2);
                    const C = new Point(p1.x + (bisectorNorm.x * centerDistance), p1.y + (bisectorNorm.y * centerDistance));
                    const startAngle = Math.atan2(T1.y - C.y, T1.x - C.x);
                    const arcSweep = Math.PI - theta;
                    const endAngle = startAngle + (cross2(T1, T2, C) < 0 ? -arcSweep : arcSweep);
                    const arcPoints = sampleArc(C, startAngle, endAngle, 10);

                    result.push(...arcPoints.slice(1));
                    prevT2 = T2;
                }
                else
                {
                    // почти 180°: просто соединяем через вершину
                    result.push(p1);
                    prevT2 = p1;
                }
            }
        }

        // --- внутренние узлы ---
        for (let i = 2; i < points.length - 1; i++)
        {
            const pPrev = points[i - 1];
            const pCurr = points[i];
            const pNext = points[i + 1];

            const v1 = normalizeVector(pPrev.x - pCurr.x, pPrev.y - pCurr.y);
            const v2 = normalizeVector(pNext.x - pCurr.x, pNext.y - pCurr.y);
            const dot = clamp((v1[0] * v2[0]) + (v1[1] * v2[1]), -1, 1);
            const theta = Math.acos(dot);

            if (theta < 1e-4)
            {
                // прямая — не вставляем T1/T2
                if (prevT2) result.push(pCurr);
                prevT2 = pCurr;
                continue;
            }

            const offset = radius / Math.tan(theta / 2);
            const T1 = new Point(pCurr.x + (v1[0] * offset), pCurr.y + (v1[1] * offset));
            const T2 = new Point(pCurr.x + (v2[0] * offset), pCurr.y + (v2[1] * offset));

            if (prevT2) result.push(T1);

            const bisector = new Point(v1[0] + v2[0], v1[1] + v2[1]);
            const bisectorLen = Math.hypot(bisector.x, bisector.y);

            if (bisectorLen > 0)
            {
                const bisectorNorm = new Point(bisector.x / bisectorLen, bisector.y / bisectorLen);
                const centerDistance = radius / Math.sin(theta / 2);
                const C = new Point(pCurr.x + (bisectorNorm.x * centerDistance), pCurr.y + (bisectorNorm.y * centerDistance));
                const startAngle = Math.atan2(T1.y - C.y, T1.x - C.x);
                const arcSweep = Math.PI - theta;
                const endAngle = startAngle + (cross2(T1, T2, C) < 0 ? -arcSweep : arcSweep);
                const arcPoints = sampleArc(C, startAngle, endAngle, 10);

                result.push(...arcPoints.slice(1));
            }
            else
            {
                result.push(pCurr);
            }
            prevT2 = T2;
        }

        // --- хвост ---
        const lastPoint = points[points.length - 1];

        if (prevT2)
        {
            result.push(prevT2, lastPoint);
        }
        else
        {
            result.push(lastPoint);
        }

        return result;
    }

    /**
     * Создает Mesh c толщиной по центральной кривой.
     * Митер-нормали с ограничением устраняют «ломы» на острых углах без обязательного скругления.
     * @param curvePoints
     * @param thickness
     * @param texture
     */
    private createThickLineMesh(curvePoints: Point[], thickness: number, texture: Texture): Mesh
    {
        const vertices: number[] = [];
        const uvs: number[] = [];
        const indices: number[] = [];
        const n = curvePoints.length;
        const halfThickness = thickness / 2;

        // накопленные длины для UV
        let totalLength = 0;
        const lengths: number[] = [0];

        for (let i = 1; i < n; i++)
        {
            const dx = curvePoints[i].x - curvePoints[i - 1].x;
            const dy = curvePoints[i].y - curvePoints[i - 1].y;
            const seg = Math.hypot(dx, dy);

            totalLength += seg;
            lengths.push(totalLength);
        }

        // —— митер-нормали ——
        const MITER_LIMIT = 4;

        function unit(x: number, y: number)
        {
            const l = Math.hypot(x, y);

            return l > 0 ? { x: x / l, y: y / l } : { x: 0, y: 0 };
        }
        function perp(v: {x: number;y: number}) { return { x: -v.y, y: v.x }; }

        function computeNormal(i: number): { x: number; y: number }
        {
            if (n === 1) return { x: 0, y: 0 };
            if (i === 0)
            {
                const d = unit(curvePoints[1].x - curvePoints[0].x, curvePoints[1].y - curvePoints[0].y);

                return perp(d);
            }
            if (i === n - 1)
            {
                const d = unit(curvePoints[n - 1].x - curvePoints[n - 2].x, curvePoints[n - 1].y - curvePoints[n - 2].y);

                return perp(d);
            }
            const dPrev = unit(curvePoints[i].x - curvePoints[i - 1].x, curvePoints[i].y - curvePoints[i - 1].y);
            const dNext = unit(curvePoints[i + 1].x - curvePoints[i].x, curvePoints[i + 1].y - curvePoints[i].y);

            const sum = { x: dPrev.x + dNext.x, y: dPrev.y + dNext.y };
            const sumLen = Math.hypot(sum.x, sum.y);

            if (sumLen < 1e-6)
            {
                // угол ~180°
                return perp(dNext);
            }
            const t = { x: sum.x / sumLen, y: sum.y / sumLen }; // усреднённая касательная
            const nAvg = perp(t);

            // miter scale = 1 / dot(nAvg, nNext)
            const nNext = perp(dNext);
            const denom = (nAvg.x * nNext.x) + (nAvg.y * nNext.y);
            let miterScale = 1 / Math.max(denom, 1e-4);

            miterScale = Math.min(miterScale, MITER_LIMIT);

            return { x: nAvg.x * miterScale, y: nAvg.y * miterScale };
        }

        // вершины + UV
        for (let i = 0; i < n; i++)
        {
            const p = curvePoints[i];
            const normal = computeNormal(i);

            vertices.push(p.x - (normal.x * halfThickness), p.y - (normal.y * halfThickness));
            vertices.push(p.x + (normal.x * halfThickness), p.y + (normal.y * halfThickness));

            const u = totalLength > 0 ? lengths[i] / totalLength : 0;

            uvs.push(u, 0);
            uvs.push(u, 1);
        }

        // индексы (два треугольника на сегмент)
        for (let i = 0; i < n - 1; i++)
        {
            const idx = i * 2;

            indices.push(idx, idx + 1, idx + 2);
            indices.push(idx + 1, idx + 3, idx + 2);
        }

        const geometry = new Geometry()
            .addAttribute('aVertexPosition', vertices, 2)
            .addAttribute('aTextureCoord', uvs, 2)
            .addIndex(indices);

        return new Mesh(geometry, new ScrollingMeshMaterial(texture));
    }

    public update(dt: number)
    {
        this.time += dt * 0.001;
        if (this.state === 'stopped')
        {
            this.visible = false;

            return;
        }

        if (this.state === 'animating')
        {
            this.visible = true;
            const material = this.mesh.material as ScrollingMeshMaterial;

            material.uniforms.uTextureOffset = 1 - (2 * this.time / this.duration);
            if (material.uniforms.uTextureOffset <= -1)
            {
                material.uniforms.uTextureOffset = 1;
                this.state = 'stopped';
            }
        }
    }

    public play()
    {
        this.state = 'animating';
        this.time = 0;
        const material = this.mesh.material as ScrollingMeshMaterial;

        material.uniforms.uTextureOffset = 1;
    }
}

/* ---------- utils ---------- */

function normalizeVector(x: number, y: number): [number, number]
{
    const length = Math.hypot(x, y);

    return length === 0 ? [0, 0] : [x / length, y / length];
}

function clamp(v: number, a: number, b: number)
{
    return Math.max(a, Math.min(b, v));
}

function cross2(a: Point, b: Point, c: Point)
{
    // cross( (A-C), (B-C) )
    return ((a.x - c.x) * (b.y - c.y)) - ((a.y - c.y) * (b.x - c.x));
}

/**
 * Удаляет дубликаты / почти-нулевые сегменты и промежуточные строго коллинеарные точки
 * @param points
 * @param eps
 */
function dedupeAndColinearCull(points: Point[], eps = 1e-4): Point[]
{
    if (points.length <= 1) return [...points];

    const out: Point[] = [];

    for (const p of points)
    {
        const last = out[out.length - 1];

        if (!last || Math.hypot(p.x - last.x, p.y - last.y) > eps) out.push(p);
    }

    if (out.length < 3) return out;

    const pruned: Point[] = [out[0]];

    for (let i = 1; i < out.length - 1; i++)
    {
        const a = out[i - 1];
        const b = out[i];
        const c = out[i + 1];
        const abx = b.x - a.x;
        const aby = b.y - a.y;
        const bcx = c.x - b.x;
        const bcy = c.y - b.y;
        const cross = (abx * bcy) - (aby * bcx);

        if (Math.abs(cross) > eps) pruned.push(b);
    }
    pruned.push(out[out.length - 1]);

    return pruned;
}
