import { constrainDimensions, getInitialPosition, CONTAINER_PADDING } from './utils/layoutUtils';

import { fetchLayoutRecords, upsertLayoutRecords } from './db';
import { CardPhysics } from './CardPhysics';

export interface LayoutCardState {
    id: string;
    x: number;
    y: number;
    z: number;
    rotation: number;
    width: number;
    height: number;
    pageCount: number;
}

export class LayoutCard implements LayoutCardState {
    id: string;
    x: number = 0;
    y: number = 0;
    z: number = 0;
    rotation: number = 0;
    width: number = 0;
    height: number = 0;
    pageCount: number = 1;
    ref: HTMLElement | null = null;

    private _innerRadius: number = 0;
    private _outerRadius: number = 0;
    private _centerX: number = 0;
    private _centerY: number = 0;
    public store: LayoutStore;
    public physics: CardPhysics;
    public intendedX: number = 0;
    public intendedY: number = 0;
    public isDirty: boolean = false;

    constructor(id: string, store: LayoutStore, initialData: Partial<LayoutCardState> = {}, ref: HTMLElement | null = null) {
        this.id = id;
        this.store = store;
        Object.assign(this, initialData);
        this.physics = new CardPhysics(this);
        this.intendedX = this.x;
        this.intendedY = this.y;
        this.ref = ref;
        this.recalculateRadii();
        this.recalculateCenters();
    }

    setRef(ref: HTMLElement | null) {
        this.ref = ref;
        this.applyTransform();
    }

    getConstrainedPosition(x: number, y: number): { x: number, y: number } {
        const rad = (this.rotation * Math.PI) / 180;
        const sin = Math.abs(Math.sin(rad));
        const cos = Math.abs(Math.cos(rad));

        const rotatedWidth = this.width * cos + this.height * sin;
        const rotatedHeight = this.width * sin + this.height * cos;

        const minX = CONTAINER_PADDING + (rotatedWidth - this.width) / 2;
        const maxX = this.store.containerWidth - CONTAINER_PADDING - this.width - (rotatedWidth - this.width) / 2;

        const minY = CONTAINER_PADDING + (rotatedHeight - this.height) / 2;
        const maxY = this.store.containerHeight - CONTAINER_PADDING - this.height - (rotatedHeight - this.height) / 2;

        const newX = Math.max(minX, Math.min(x, maxX));
        const newY = Math.max(minY, Math.min(y, maxY));

        return { x: newX, y: newY };
    }



    update(changes: Partial<LayoutCardState>, options: { markDirty?: boolean, isConstraintUpdate?: boolean } = {}) {
        Object.assign(this, changes);

        if (!options.isConstraintUpdate) {
            if (changes.x !== undefined) this.intendedX = changes.x;
            if (changes.y !== undefined) this.intendedY = changes.y;
        }

        if (changes.width !== undefined || changes.height !== undefined) {
            this.recalculateRadii();
        }

        if (changes.x !== undefined || changes.y !== undefined || changes.width !== undefined || changes.height !== undefined) {
            this.recalculateCenters();
        }

        if (changes.pageCount !== undefined) {
            this.physics.updateMass(changes.pageCount);
        }

        if (options.markDirty) {
            this.isDirty = true;
        }

        this.applyTransform();
    }

    isUnobstructed(): boolean {
        for (const other of this.store.items.values()) {
            if (other.id === this.id) continue;
            if (other.z <= this.z) continue;

            const dx = other.x - this.x;
            const dy = other.y - this.y;
            const distance = Math.sqrt(dx * dx + dy * dy);

            // Broad phase: check outer radii
            if (distance < this.outerRadius + other.outerRadius) {
                // Narrow phase: SAT intersection test
                if (this.intersects(other)) {
                    return false;
                }
            }
        }

        return true;
    }

    bringToFront() {
        this.z = this.store.zCounter++;
    }

    private getVertices(): { x: number; y: number }[] {
        const rad = (this.rotation * Math.PI) / 180;
        const cos = Math.cos(rad);
        const sin = Math.sin(rad);
        const hw = this.width / 2;
        const hh = this.height / 2;

        // Corners relative to center, then rotated, then translated
        // (-hw, -hh), (hw, -hh), (hw, hh), (-hw, hh)
        const corners = [
            { x: -hw, y: -hh },
            { x: hw, y: -hh },
            { x: hw, y: hh },
            { x: -hw, y: hh }
        ];

        return corners.map(p => ({
            x: (p.x * cos - p.y * sin) + this._centerX,
            y: (p.x * sin + p.y * cos) + this._centerY
        }));
    }

    containsPoint(x: number, y: number): boolean {
        // Translate point to local space relative to center
        const dx = x - this._centerX;
        const dy = y - this._centerY;

        // Rotate point by -rotation to align with AABB
        const rad = (-this.rotation * Math.PI) / 180;
        const cos = Math.cos(rad);
        const sin = Math.sin(rad);

        const localX = dx * cos - dy * sin;
        const localY = dx * sin + dy * cos;

        const hw = this.width / 2;
        const hh = this.height / 2;

        return localX >= -hw && localX <= hw && localY >= -hh && localY <= hh;
    }

    getVisibleFraction(): number {
        const samplesX = 4;
        const samplesY = 4;
        const totalSamples = samplesX * samplesY;
        let visibleSamples = 0;

        // Get potential occluders (higher Z-index)
        const occluders = Array.from(this.store.items.values()).filter(other =>
            other.id !== this.id && other.z > this.z
        );

        if (occluders.length === 0) return 1.0;

        const rad = (this.rotation * Math.PI) / 180;
        const cos = Math.cos(rad);
        const sin = Math.sin(rad);
        const hw = this.width / 2;
        const hh = this.height / 2;

        // Sample points across the card surface
        for (let i = 0; i < samplesX; i++) {
            for (let j = 0; j < samplesY; j++) {
                // Normalized coordinates [-1, 1]
                const nx = (i / (samplesX - 1)) * 2 - 1;
                const ny = (j / (samplesY - 1)) * 2 - 1;

                // Local coordinates
                const lx = nx * hw * 0.9; // 0.9 to avoid edge cases
                const ly = ny * hh * 0.9;

                // World coordinates
                const wx = (lx * cos - ly * sin) + this._centerX;
                const wy = (lx * sin + ly * cos) + this._centerY;

                // Check occlusion
                let isOccluded = false;
                for (const occluder of occluders) {
                    if (occluder.containsPoint(wx, wy)) {
                        isOccluded = true;
                        break;
                    }
                }

                if (!isOccluded) {
                    visibleSamples++;
                }
            }
        }

        return visibleSamples / totalSamples;
    }

    private getAxes(): { x: number; y: number }[] {
        const rad = (this.rotation * Math.PI) / 180;
        const cos = Math.cos(rad);
        const sin = Math.sin(rad);
        // Normals of the edges (local x and y axes)
        return [
            { x: cos, y: sin },
            { x: -sin, y: cos }
        ];
    }

    private intersects(other: LayoutCard): boolean {
        const verticesA = this.getVertices();
        const verticesB = other.getVertices();
        const axes = [...this.getAxes(), ...other.getAxes()];

        for (const axis of axes) {
            const pA = this.project(verticesA, axis);
            const pB = this.project(verticesB, axis);

            if (pA.max < pB.min || pB.max < pA.min) {
                return false; // Gap found, no intersection
            }
        }
        return true;
    }

    private project(vertices: { x: number; y: number }[], axis: { x: number; y: number }) {
        let min = Infinity;
        let max = -Infinity;
        for (const v of vertices) {
            const dot = v.x * axis.x + v.y * axis.y;
            if (dot < min) min = dot;
            if (dot > max) max = dot;
        }
        return { min, max };
    }

    private recalculateRadii() {
        this._innerRadius = Math.min(this.width, this.height) / 2;
        this._outerRadius = Math.sqrt(this.width * this.width + this.height * this.height) / 2;
    }

    private recalculateCenters() {
        this._centerX = this.x + this.width / 2;
        this._centerY = this.y + this.height / 2;
    }

    private rafId: number | null = null;

    private applyTransform() {
        if (this.rafId) {
            cancelAnimationFrame(this.rafId);
        }

        this.rafId = requestAnimationFrame(() => {
            if (this.ref) {
                this.ref.style.transform =
                    `translate3d(${this.x}px, ${this.y}px, 0) rotate(${this.rotation}deg)`;
                this.ref.style.zIndex = String(this.z);
                this.ref.style.width = `${this.width}px`;
                this.ref.style.height = `${this.height}px`;
            }
            this.rafId = null;
        });
    }

    snapTo(x: number, y: number) {
        this.update({ x, y }, { markDirty: true });

        if (!this.ref)
            return;

        this.ref.classList.add('desk-item--swoop');
        this.ref.style.transform = `translate3d(${this.x}px, ${this.y}px, 0) rotate(${this.rotation}deg)`;

        const cleanup = () => {
            if (this.ref) {
                this.ref.classList.remove('desk-item--swoop');
            }
        };

        this.ref.addEventListener('transitionend', cleanup, { once: true });

        // Safety timeout in case transitionend doesn't fire (e.g. element removed)
        setTimeout(cleanup, 350);
    }

    toSnapshot(): LayoutCardState {
        return {
            id: this.id,
            x: this.x,
            y: this.y,
            z: this.z,
            rotation: this.rotation,
            width: this.width,
            height: this.height,
            pageCount: this.pageCount
        };
    }

    get innerRadius(): number {
        return this._innerRadius;
    }

    get outerRadius(): number {
        return this._outerRadius;
    }

    get centerX(): number {
        return this._centerX;
    }

    get centerY(): number {
        return this._centerY;
    }

    get isDragging(): boolean {
        return this.physics.isDragging;
    }
}

export class LayoutStore {
    items = new Map<string, LayoutCard>();
    zCounter = 100;
    containerWidth: number = 0;
    containerHeight: number = 0;

    private savedLayouts = new Map<string, { x: number, y: number, rotation: number, z: number }>();
    private tenantId: string | null = null;
    private viewId: string | null = null;

    initialize(id: string, ref: HTMLElement | null, config: {
        width: number;
        height: number;
        pageCount: number;
    }) {
        let card = this.items.get(id);

        const { width, height } = constrainDimensions(
            config.width,
            config.height,
            320
        );

        if (!card) {
            // Check for saved layout
            const saved = this.savedLayouts.get(id);

            let x, y, rotation, z;

            if (saved) {
                x = saved.x;
                y = saved.y;
                rotation = saved.rotation;
                z = saved.z;
                // Ensure zCounter is higher than any loaded z
                if (z >= this.zCounter) {
                    this.zCounter = z + 1;
                }

                card = new LayoutCard(id, this, {
                    x,
                    y,
                    rotation,
                    width,
                    height,
                    z,
                    pageCount: config.pageCount
                }, ref);
            } else {
                // Create card with temporary position
                card = new LayoutCard(id, this, {
                    width,
                    height,
                    z: this.zCounter++,
                    pageCount: config.pageCount
                }, ref);

                // Calculate initial position using the card instance
                const { x: initX, y: initY, rotation: initRotation } = getInitialPosition(
                    this.containerWidth,
                    this.containerHeight,
                    card,
                    Array.from(this.items.values())
                );

                // Update card with calculated position
                card.update({ x: initX, y: initY, rotation: initRotation }, { markDirty: true });
            }

            this.items.set(id, card);
        } else {
            card.update({ width, height, pageCount: config.pageCount }, { markDirty: false });
        }

        // Always update ref and ensure transform is applied
        if (card.ref !== ref) {
            card.setRef(ref);
        }

        return card;
    }

    unregister(id: string) {
        this.items.delete(id);
    }

    clear() {
        this.items.clear();
        this.zCounter = 100;
        this.savedLayouts.clear();
    }

    setContainerSize(width: number, height: number) {
        this.containerWidth = width;
        this.containerHeight = height;
        this.relayout();
    }

    relayout() {
        for (const card of this.items.values()) {
            const { x, y } = card.getConstrainedPosition(card.intendedX, card.intendedY);
            if (x !== card.x || y !== card.y) {
                card.update({ x, y }, { markDirty: false, isConstraintUpdate: true });
            }
        }
    }

    getCardsInCircle(x: number, y: number, radius: number): LayoutCard[] {
        const result: LayoutCard[] = [];
        for (const card of this.items.values()) {
            // Calculate center of candidate card
            const cx = card.x + card.width / 2;
            const cy = card.y + card.height / 2;

            const dx = cx - x;
            const dy = cy - y;

            const distSq = dx * dx + dy * dy;
            const limit = radius;

            if (distSq < limit * limit) {
                result.push(card);
            }
        }
        return result;
    }

    getStackBelow(topCard: LayoutCard): string[] {
        const centerX = topCard.x + topCard.width / 2;
        const centerY = topCard.y + topCard.height / 2;
        const candidates = this.getCardsInCircle(centerX, centerY, topCard.innerRadius);

        return candidates
            .filter(other => {
                if (other.id === topCard.id) return false;
                if (other.z >= topCard.z) return false;
                return true;
            })
            .map(c => c.id);
    }

    bringToFront(ids: string[]) {
        const cards = ids
            .map(id => this.items.get(id))
            .filter((c): c is LayoutCard => !!c);

        // Sort by current Z-index to preserve relative order
        cards.sort((a, b) => a.z - b.z);

        // Assign new Z-indices
        for (const card of cards) {
            card.update({ z: this.zCounter++ }, { markDirty: true });
        }
    }

    getSnapshot() {
        return Array.from(this.items.values()).map(card => card.toSnapshot());
    }

    async loadLayout(tenantId: string, viewId: string) {
        this.tenantId = tenantId;
        this.viewId = viewId;

        const records = await fetchLayoutRecords({ tenantId, viewId });

        this.savedLayouts.clear();
        let maxZ = this.zCounter;

        for (const record of records) {
            if (record.documentId && record.centerX !== undefined && record.centerY !== undefined) {
                this.savedLayouts.set(record.documentId, {
                    x: record.centerX,
                    y: record.centerY,
                    rotation: record.rotation || 0,
                    z: record.zIndex || 0
                });
                if (record.zIndex && record.zIndex >= maxZ) {
                    maxZ = record.zIndex + 1;
                }
            }
        }

        this.zCounter = maxZ;

        // Apply to existing items if any (though usually this runs before items are created)
        for (const [id, card] of this.items) {
            const saved = this.savedLayouts.get(id);
            if (saved) {
                card.update(saved, { markDirty: false });
            }
        }

        // Ensure everything is within bounds
        this.relayout();
    }

    async saveLayout() {
        if (!this.tenantId || !this.viewId) return;

        const dirtyCards = Array.from(this.items.values()).filter(card => card.isDirty);
        if (dirtyCards.length === 0) return;

        const entries = dirtyCards.map(card => ({
            documentId: card.id,
            centerX: card.intendedX,
            centerY: card.intendedY,
            rotation: card.rotation,
            zIndex: card.z,
            updatedAt: Date.now()
        }));

        await upsertLayoutRecords({
            tenantId: this.tenantId,
            viewId: this.viewId,
            entries
        });

        // Reset dirty flag for saved cards
        for (const card of dirtyCards) {
            card.isDirty = false;
        }
    }
}
