import { FIRST_OR_NIL, IS, IS_DEFINED, IS_NIL, IS_NOT_LIKE_NULL, IS_NOT_NIL, nil, NO, UIObject, YES } from "./UIObject"
import { UIPoint } from "./UIPoint"
import { UIView } from "./UIView"


export type SizeNumberOrFunctionOrView = number | ((constrainingOrthogonalSize: number) => number) | UIView

export class UIRectangle extends UIObject {
    
    _isBeingUpdated: boolean
    rectanglePointDidChange?: (b: any) => void
    
    // COW: Internal data structure that can be shared
    private _data: {
        min: UIPoint
        max: UIPoint
        minHeight?: number
        maxHeight?: number
        minWidth?: number
        maxWidth?: number
        refCount: number
    }
    
    // COW: Flag to indicate this is a lazy copy
    private _isLazyCopy: boolean
    
    
    constructor(x: number = 0, y: number = 0, height: number = 0, width: number = 0) {
        
        super()
        
        this._isLazyCopy = NO
        this._isBeingUpdated = NO
        
        // COW: Create the shared data structure
        this._data = {
            min: new UIPoint(x, y),
            max: new UIPoint(x + width, y + height),
            refCount: 1
        }
        
        this._setupPointCallbacks()
        
        if (IS_NIL(height)) {
            this._data.max.y = height
        }
        
        if (IS_NIL(width)) {
            this._data.max.x = width
        }
        
    }
    
    // COW: Setup callbacks for point changes
    private _setupPointCallbacks() {
        this._data.min.didChange = (point) => {
            this.rectanglePointDidChange?.(point)
            this._rectanglePointDidChange()
        }
        this._data.max.didChange = (point) => {
            this.rectanglePointDidChange?.(point)
            this._rectanglePointDidChange()
        }
    }
    
    // COW: Materialize a lazy copy before mutation
    materialize() {
        if (this._isLazyCopy || this._data.refCount > 1) {
            this._data.refCount--
            
            const oldData = this._data
            this._data = {
                min: oldData.min.copy(),
                max: oldData.max.copy(),
                minHeight: oldData.minHeight,
                maxHeight: oldData.maxHeight,
                minWidth: oldData.minWidth,
                maxWidth: oldData.maxWidth,
                refCount: 1
            }
            
            this._setupPointCallbacks()
            this._isLazyCopy = NO
        }
    }
    
    // Copy on write: Lazy copy that shares data
    // Tested to reduce CPU time from 2.2% to 1% during heavy resizing
    lazyCopy(): UIRectangle {
        const result = Object.create(UIRectangle.prototype)
        result._data = this._data
        result._data.refCount++
        result._isLazyCopy = YES
        result._isBeingUpdated = NO
        result.rectanglePointDidChange = this.rectanglePointDidChange
        return result
    }
    
    // COW: Getters and setters that materialize on write
    get min(): UIPoint {
        return this._data.min
    }
    
    set min(value: UIPoint) {
        this.materialize()
        this._data.min = value
        this._setupPointCallbacks()
    }
    
    get max(): UIPoint {
        return this._data.max
    }
    
    set max(value: UIPoint) {
        this.materialize()
        this._data.max = value
        this._setupPointCallbacks()
    }
    
    get minHeight(): number | undefined {
        return this._data.minHeight
    }
    
    set minHeight(value: number | undefined) {
        this.materialize()
        this._data.minHeight = value
    }
    
    get maxHeight(): number | undefined {
        return this._data.maxHeight
    }
    
    set maxHeight(value: number | undefined) {
        this.materialize()
        this._data.maxHeight = value
    }
    
    get minWidth(): number | undefined {
        return this._data.minWidth
    }
    
    set minWidth(value: number | undefined) {
        this.materialize()
        this._data.minWidth = value
    }
    
    get maxWidth(): number | undefined {
        return this._data.maxWidth
    }
    
    set maxWidth(value: number | undefined) {
        this.materialize()
        this._data.maxWidth = value
    }
    
    
    copy() {
        
        const result = new UIRectangle(this.x, this.y, this.height, this.width)
        
        result.minHeight = this.minHeight
        result.minWidth = this.minWidth
        result.maxHeight = this.maxHeight
        result.maxWidth = this.maxWidth
        
        return result
        
    }
    
    isEqualTo(rectangle: UIRectangle | null | undefined) {
        return (IS(rectangle) && this.min.isEqualTo(rectangle.min) && this.max.isEqualTo(rectangle.max))
    }
    
    static zero() {
        return new UIRectangle(0, 0, 0, 0)
    }
    
    containsPoint(point: UIPoint) {
        return this.min.x <= point.x && this.min.y <= point.y &&
            point.x <= this.max.x && point.y <= this.max.y
    }
    
    updateByAddingPoint(point: UIPoint) {
        
        this.materialize() // COW: Materialize before mutation
        
        if (!point) {
            point = new UIPoint(0, 0)
        }
        
        this.beginUpdates()
        
        const min = this.min.copy()
        if (min.x === nil) {
            min.x = this.max.x
        }
        if (min.y === nil) {
            min.y = this.max.y
        }
        
        const max = this.max.copy()
        if (max.x === nil) {
            max.x = this.min.x
        }
        if (max.y === nil) {
            max.y = this.min.y
        }
        
        this.min.x = Math.min(min.x, point.x)
        this.min.y = Math.min(min.y, point.y)
        this.max.x = Math.max(max.x, point.x)
        this.max.y = Math.max(max.y, point.y)
        
        this.finishUpdates()
        
    }
    
    scale(scale: number) {
        this.materialize() // COW: Materialize before mutation
        if (IS_NOT_NIL(this.max.y)) {
            this.height = this.height * scale
        }
        if (IS_NOT_NIL(this.max.x)) {
            this.width = this.width * scale
        }
    }
    
    get height() {
        if (this.max.y === nil) {
            return nil
        }
        return this.max.y - this.min.y
    }
    
    set height(height: number) {
        this.materialize() // COW: Materialize before mutation
        this._data.max.y = this.min.y + height
    }
    
    
    get width() {
        if (this.max.x === nil) {
            return nil
        }
        return this.max.x - this.min.x
    }
    
    set width(width: number) {
        this.materialize() // COW: Materialize before mutation
        this._data.max.x = this.min.x + width
    }
    
    
    get x() {
        return this.min.x
    }
    
    set x(x: number) {
        
        this.materialize() // COW: Materialize before mutation
        this.beginUpdates()
        
        const width = this.width
        this._data.min.x = x
        this._data.max.x = this.min.x + width
        
        this.finishUpdates()
        
    }
    
    
    get y() {
        return this.min.y
    }
    
    
    set y(y: number) {
        
        this.materialize() // COW: Materialize before mutation
        this.beginUpdates()
        
        const height = this.height
        this._data.min.y = y
        this._data.max.y = this.min.y + height
        
        this.finishUpdates()
        
    }
    
    
    get topLeft() {
        return this.min.copy()
    }
    
    get topRight() {
        return new UIPoint(this.max.x, this.y)
    }
    
    get bottomLeft() {
        return new UIPoint(this.x, this.max.y)
    }
    
    get bottomRight() {
        return this.max.copy()
    }
    
    
    get center() {
        return this.min.copy().add(this.min.to(this.max).scale(0.5))
    }
    
    set center(center: UIPoint) {
        this.materialize() // COW: Materialize before mutation
        const offset = this.center.to(center)
        this.offsetByPoint(offset)
    }
    
    offsetByPoint(offset: UIPoint) {
        this.materialize() // COW: Materialize before mutation
        this.min.add(offset)
        this.max.add(offset)
        
        return this
    }
    
    
    concatenateWithRectangle(rectangle: UIRectangle) {
        this.updateByAddingPoint(rectangle.bottomRight)
        this.updateByAddingPoint(rectangle.topLeft)
        return this
    }
    
    rectangleByConcatenatingWithRectangle(rectangle: UIRectangle) {
        return this.lazyCopy().concatenateWithRectangle(rectangle) // COW: Use lazyCopy
    }
    
    
    intersectionRectangleWithRectangle(rectangle: UIRectangle): UIRectangle {
        
        const result = this.lazyCopy() // COW: Use lazyCopy
        result.materialize() // COW: We're going to modify it
        
        result.beginUpdates()
        
        const min = result.min
        if (min.x === nil) {
            min.x = rectangle.max.x - Math.min(result.width, rectangle.width)
        }
        if (min.y === nil) {
            min.y = rectangle.max.y - Math.min(result.height, rectangle.height)
        }
        
        const max = result.max
        if (max.x === nil) {
            max.x = rectangle.min.x + Math.min(result.width, rectangle.width)
        }
        if (max.y === nil) {
            max.y = rectangle.min.y + Math.min(result.height, rectangle.height)
        }
        
        result.min.x = Math.max(result.min.x, rectangle.min.x)
        result.min.y = Math.max(result.min.y, rectangle.min.y)
        result.max.x = Math.min(result.max.x, rectangle.max.x)
        result.max.y = Math.min(result.max.y, rectangle.max.y)
        
        
        if (result.height < 0) {
            
            const averageY = (this.center.y + rectangle.center.y) * 0.5
            result.min.y = averageY
            result.max.y = averageY
            
        }
        
        if (result.width < 0) {
            
            const averageX = (this.center.x + rectangle.center.x) * 0.5
            result.min.x = averageX
            result.max.x = averageX
            
        }
        
        result.finishUpdates()
        
        return result
        
    }
    
    
    get area() {
        return this.height * this.width
    }
    
    
    intersectsWithRectangle(rectangle: UIRectangle) {
        return (this.intersectionRectangleWithRectangle(rectangle).area != 0)
    }
    
    
    // add some space around the rectangle
    rectangleWithInsets(left: number, right: number, bottom: number, top: number) {
        const result = this.lazyCopy() // COW: Use lazyCopy
        result.materialize() // COW: We're modifying multiple properties
        result.min.x = this.min.x + left
        result.max.x = this.max.x - right
        result.min.y = this.min.y + top
        result.max.y = this.max.y - bottom
        return result
    }
    
    rectangleWithInset(inset: number) {
        return this.rectangleWithInsets(inset, inset, inset, inset)
    }
    
    rectangleWithHeight(height: SizeNumberOrFunctionOrView, centeredOnPosition: number = nil) {
        
        height = this._heightNumberFromSizeNumberOrFunctionOrView(height)
        
        if (isNaN(centeredOnPosition)) {
            centeredOnPosition = nil
        }
        
        const result = this.lazyCopy() // COW: Use lazyCopy
        result.height = height
        
        if (centeredOnPosition != nil) {
            const change = height - this.height
            result.offsetByPoint(new UIPoint(0, change * centeredOnPosition).scale(-1))
        }
        
        return result
        
    }
    
    rectangleWithWidth(width: SizeNumberOrFunctionOrView, centeredOnPosition: number = nil) {
        
        width = this._widthNumberFromSizeNumberOrFunctionOrView(width)
        
        if (isNaN(centeredOnPosition)) {
            centeredOnPosition = nil
        }
        
        const result = this.lazyCopy() // COW: Use lazyCopy
        result.width = width
        
        if (centeredOnPosition != nil) {
            const change = width - this.width
            result.offsetByPoint(new UIPoint(change * centeredOnPosition, 0).scale(-1))
        }
        
        return result
        
    }
    
    rectangleWithHeightRelativeToWidth(heightRatio: number = 1, centeredOnPosition: number = nil) {
        return this.rectangleWithHeight(this.width * heightRatio, centeredOnPosition)
    }
    
    rectangleWithWidthRelativeToHeight(widthRatio: number = 1, centeredOnPosition: number = nil) {
        return this.rectangleWithWidth(this.height * widthRatio, centeredOnPosition)
    }
    
    rectangleWithX(x: number, centeredOnPosition: number = 0) {
        
        const result = this.lazyCopy() // COW: Use lazyCopy
        result.x = x - result.width * centeredOnPosition
        
        return result
        
    }
    
    rectangleWithY(y: number, centeredOnPosition: number = 0) {
        
        const result = this.lazyCopy() // COW: Use lazyCopy
        result.y = y - result.height * centeredOnPosition
        
        return result
        
    }
    
    
    rectangleByAddingX(x: number) {
        
        const result = this.lazyCopy() // COW: Use lazyCopy
        result.x = this.x + x
        
        return result
        
    }
    
    rectangleByAddingY(y: number) {
        
        const result = this.lazyCopy() // COW: Use lazyCopy
        result.y = this.y + y
        
        return result
        
    }
    
    rectangleByAddingWidth(widthToAdd: number, centeredOnPosition = 0) {
        
        const result = this.rectangleWithWidth(this.width + widthToAdd, centeredOnPosition)
        
        return result
        
    }
    
    rectangleByAddingHeight(heightToAdd: number, centeredOnPosition = 0) {
        
        const result = this.rectangleWithHeight(this.height + heightToAdd, centeredOnPosition)
        
        return result
        
    }
    
    
    rectangleWithRelativeValues(
        relativeXPosition: number,
        widthMultiplier: number,
        relativeYPosition: number,
        heightMultiplier: number
    ) {
        
        const result = this.lazyCopy() // COW: Use lazyCopy
        result.materialize() // COW: We're modifying multiple properties
        
        const width = result.width
        const height = result.height
        
        result.width = widthMultiplier * width
        result.height = heightMultiplier * height
        
        result.center = new UIPoint(
            relativeXPosition * width,
            relativeYPosition * height
        )
        
        return result
        
    }
    
    
    /**
     * Returns a rectangle with a maximum width constraint
     * If current width exceeds max, centers the constrained width
     */
    rectangleWithMaxWidth(maxWidth: number, centeredOnPosition: number = 0): UIRectangle {
        if (this.width <= maxWidth) {
            return this.lazyCopy() // COW: Use lazyCopy
        }
        return this.rectangleWithWidth(maxWidth, centeredOnPosition)
    }
    
    /**
     * Returns a rectangle with a maximum height constraint
     */
    rectangleWithMaxHeight(maxHeight: number, centeredOnPosition: number = 0): UIRectangle {
        if (this.height <= maxHeight) {
            return this.lazyCopy() // COW: Use lazyCopy
        }
        return this.rectangleWithHeight(maxHeight, centeredOnPosition)
    }
    
    /**
     * Returns a rectangle with minimum width constraint
     */
    rectangleWithMinWidth(minWidth: number, centeredOnPosition: number = 0): UIRectangle {
        if (this.width >= minWidth) {
            return this.lazyCopy() // COW: Use lazyCopy
        }
        return this.rectangleWithWidth(minWidth, centeredOnPosition)
    }
    
    /**
     * Returns a rectangle with minimum height constraint
     */
    rectangleWithMinHeight(minHeight: number, centeredOnPosition: number = 0): UIRectangle {
        if (this.height >= minHeight) {
            return this.lazyCopy() // COW: Use lazyCopy
        }
        return this.rectangleWithHeight(minHeight, centeredOnPosition)
    }
    
    // Returns a new rectangle that is positioned relative to the reference rectangle
    // By default, it makes a copy of this rectangle taht is centered in the target rectangle
    rectangleByCenteringInRectangle(referenceRectangle: UIRectangle, xPosition = 0.5, yPosition = 0.5) {
        const result = this.lazyCopy() // COW: Use lazyCopy
        result.center = referenceRectangle.topLeft
            .pointByAddingX(xPosition * referenceRectangle.width)
            .pointByAddingY(yPosition * referenceRectangle.height)
        return result
    }
    
    
    rectanglesBySplittingWidth(
        weights: SizeNumberOrFunctionOrView[],
        paddings: SizeNumberOrFunctionOrView | SizeNumberOrFunctionOrView[] = 0,
        absoluteWidths: SizeNumberOrFunctionOrView | SizeNumberOrFunctionOrView[] = nil
    ) {
        
        if (IS_NIL(paddings)) {
            paddings = 1
        }
        if (!((paddings as any) instanceof Array)) {
            paddings = [paddings].arrayByRepeating(weights.length - 1)
        }
        paddings = (paddings as any[]).arrayByTrimmingToLengthIfLonger(weights.length - 1)
        paddings = paddings.map(padding => this._widthNumberFromSizeNumberOrFunctionOrView(padding))
        if (!(absoluteWidths instanceof Array) && IS_NOT_NIL(absoluteWidths)) {
            absoluteWidths = [absoluteWidths].arrayByRepeating(weights.length)
        }
        absoluteWidths = absoluteWidths.map(
            width => this._widthNumberFromSizeNumberOrFunctionOrView(width)
        )
        
        weights = weights.map(weight => this._widthNumberFromSizeNumberOrFunctionOrView(weight))
        const result: UIRectangle[] = []
        const sumOfWeights = (weights as number[]).reduce(
            (a, b, index) => {
                if (IS_NOT_NIL((absoluteWidths as number[])[index])) {
                    b = 0
                }
                return a + b
            },
            0
        )
        const sumOfPaddings = paddings.summedValue as number
        const sumOfAbsoluteWidths = (absoluteWidths as number[]).summedValue
        const totalRelativeWidth = this.width - sumOfPaddings - sumOfAbsoluteWidths
        let previousCellMaxX = this.x
        
        for (let i = 0; i < weights.length; i++) {
            
            let resultWidth: number
            if (IS_NOT_NIL(absoluteWidths[i])) {
                resultWidth = (absoluteWidths[i] || 0) as number
            }
            else {
                resultWidth = totalRelativeWidth * (weights[i] as number / sumOfWeights)
            }
            
            const rectangle = this.rectangleWithWidth(resultWidth)
            
            let padding = 0
            if (paddings.length > i && paddings[i]) {
                padding = paddings[i] as number
            }
            
            rectangle.x = previousCellMaxX
            previousCellMaxX = rectangle.max.x + padding
            result.push(rectangle)
            
        }
        
        return result
        
    }
    
    rectanglesBySplittingHeight(
        weights: SizeNumberOrFunctionOrView[],
        paddings: SizeNumberOrFunctionOrView | SizeNumberOrFunctionOrView[] = 0,
        absoluteHeights: SizeNumberOrFunctionOrView | SizeNumberOrFunctionOrView[] = nil
    ) {
        
        if (IS_NIL(paddings)) {
            paddings = 1
        }
        if (!((paddings as any) instanceof Array)) {
            paddings = [paddings].arrayByRepeating(weights.length - 1)
        }
        paddings = (paddings as number[]).arrayByTrimmingToLengthIfLonger(weights.length - 1)
        paddings = paddings.map(padding => this._heightNumberFromSizeNumberOrFunctionOrView(padding))
        if (!(absoluteHeights instanceof Array) && IS_NOT_NIL(absoluteHeights)) {
            absoluteHeights = [absoluteHeights].arrayByRepeating(weights.length)
        }
        absoluteHeights = absoluteHeights.map(
            height => this._heightNumberFromSizeNumberOrFunctionOrView(height)
        )
        
        weights = weights.map(weight => this._heightNumberFromSizeNumberOrFunctionOrView(weight))
        const result: UIRectangle[] = []
        const sumOfWeights = (weights as number[]).reduce(
            (a, b, index) => {
                if (IS_NOT_NIL((absoluteHeights as number[])[index])) {
                    b = 0
                }
                return a + b
            },
            0
        )
        const sumOfPaddings = paddings.summedValue as number
        const sumOfAbsoluteHeights = (absoluteHeights as number[]).summedValue
        const totalRelativeHeight = this.height - sumOfPaddings - sumOfAbsoluteHeights
        let previousCellMaxY = this.y
        
        for (let i = 0; i < weights.length; i++) {
            let resultHeight: number
            if (IS_NOT_NIL(absoluteHeights[i])) {
                
                resultHeight = (absoluteHeights[i] || 0) as number
                
            }
            else {
                
                resultHeight = totalRelativeHeight * (weights[i] as number / sumOfWeights)
                
            }
            
            const rectangle = this.rectangleWithHeight(resultHeight)
            
            let padding = 0
            if (paddings.length > i && paddings[i]) {
                padding = paddings[i] as number
            }
            
            rectangle.y = previousCellMaxY
            previousCellMaxY = rectangle.max.y + padding
            //rectangle = rectangle.rectangleWithInsets(0, 0, padding, 0);
            result.push(rectangle)
        }
        
        return result
        
    }
    
    
    rectanglesByEquallySplittingWidth(numberOfFrames: number, padding: number = 0) {
        const result: UIRectangle[] = []
        const totalPadding = padding * (numberOfFrames - 1)
        const resultWidth = (this.width - totalPadding) / numberOfFrames
        for (var i = 0; i < numberOfFrames; i++) {
            const rectangle = this.rectangleWithWidth(resultWidth, i / (numberOfFrames - 1))
            result.push(rectangle)
        }
        return result
    }
    
    rectanglesByEquallySplittingHeight(numberOfFrames: number, padding: number = 0) {
        const result: UIRectangle[] = []
        const totalPadding = padding * (numberOfFrames - 1)
        const resultHeight = (this.height - totalPadding) / numberOfFrames
        for (var i = 0; i < numberOfFrames; i++) {
            const rectangle = this.rectangleWithHeight(resultHeight, i / (numberOfFrames - 1))
            result.push(rectangle)
        }
        return result
    }
    
    
    distributeViewsAlongWidth(
        views: UIView[],
        weights: SizeNumberOrFunctionOrView | SizeNumberOrFunctionOrView[] = 1,
        paddings?: SizeNumberOrFunctionOrView | SizeNumberOrFunctionOrView[],
        absoluteWidths?: SizeNumberOrFunctionOrView | SizeNumberOrFunctionOrView[]
    ) {
        if (!(weights instanceof Array)) {
            weights = [weights].arrayByRepeating(views.length)
        }
        const frames = this.rectanglesBySplittingWidth(weights, paddings, absoluteWidths)
        frames.forEach((frame, index) => FIRST_OR_NIL(views[index]).frame = frame)
        return this
    }
    
    distributeViewsAlongHeight(
        views: UIView[],
        weights: SizeNumberOrFunctionOrView | SizeNumberOrFunctionOrView[] = 1,
        paddings?: SizeNumberOrFunctionOrView | SizeNumberOrFunctionOrView[],
        absoluteHeights?: SizeNumberOrFunctionOrView | SizeNumberOrFunctionOrView[]
    ) {
        if (!(weights instanceof Array)) {
            weights = [weights].arrayByRepeating(views.length)
        }
        const frames = this.rectanglesBySplittingHeight(weights, paddings, absoluteHeights)
        frames.forEach((frame, index) => FIRST_OR_NIL(views[index]).frame = frame)
        return this
    }
    
    
    distributeViewsEquallyAlongWidth(views: UIView[], padding: number) {
        const frames = this.rectanglesByEquallySplittingWidth(views.length, padding)
        frames.forEach((frame, index) => views[index].frame = frame)
        return this
    }
    
    distributeViewsEquallyAlongHeight(views: UIView[], padding: number) {
        const frames = this.rectanglesByEquallySplittingHeight(views.length, padding)
        frames.forEach((frame, index) => views[index].frame = frame)
        return this
    }
    
    
    _heightNumberFromSizeNumberOrFunctionOrView(height: SizeNumberOrFunctionOrView) {
        if (height instanceof Function) {
            return height(this.width)
        }
        if (height instanceof UIView) {
            return height.intrinsicContentHeight(this.width)
        }
        return height
    }
    
    _widthNumberFromSizeNumberOrFunctionOrView(width: SizeNumberOrFunctionOrView) {
        if (width instanceof Function) {
            return width(this.height)
        }
        if (width instanceof UIView) {
            return width.intrinsicContentWidth(this.height)
        }
        return width
    }
    
    rectangleForNextRow(padding: number = 0, height: SizeNumberOrFunctionOrView = this.height) {
        const heightNumber = this._heightNumberFromSizeNumberOrFunctionOrView(height)
        const result = this.rectangleWithY(this.max.y + padding)
        if (heightNumber != this.height) {
            result.height = heightNumber
        }
        return result
    }
    
    rectangleForNextColumn(padding: number = 0, width: SizeNumberOrFunctionOrView = this.width) {
        const widthNumber = this._widthNumberFromSizeNumberOrFunctionOrView(width)
        const result = this.rectangleWithX(this.max.x + padding)
        if (widthNumber != this.width) {
            result.width = widthNumber
        }
        return result
    }
    
    rectangleForPreviousRow(padding: number = 0, height: SizeNumberOrFunctionOrView = this.height) {
        const heightNumber = this._heightNumberFromSizeNumberOrFunctionOrView(height)
        const result = this.rectangleWithY(this.min.y - heightNumber - padding)
        if (heightNumber != this.height) {
            result.height = heightNumber
        }
        return result
    }
    
    rectangleForPreviousColumn(padding: number = 0, width: SizeNumberOrFunctionOrView = this.width) {
        const widthNumber = this._widthNumberFromSizeNumberOrFunctionOrView(width)
        const result = this.rectangleWithX(this.min.x - widthNumber - padding)
        if (widthNumber != this.width) {
            result.width = widthNumber
        }
        return result
        
    }
    
    /**
     * Distributes views vertically as a column, assigning frames and returning them.
     * Each view is positioned below the previous one with optional padding between them.
     * @param views - Array of views to distribute
     * @param paddings - Padding between views (single value or array of values)
     * @param absoluteHeights - Optional fixed heights for views (overrides intrinsic height)
     * @returns Array of rectangles representing the frame for each view
     */
    framesByDistributingViewsAsColumn(
        views: UIView[],
        paddings: SizeNumberOrFunctionOrView | SizeNumberOrFunctionOrView[] = 0,
        absoluteHeights: SizeNumberOrFunctionOrView | SizeNumberOrFunctionOrView[] = nil
    ) {
        const frames: UIRectangle[] = []
        let currentRectangle = this.lazyCopy() // COW: Use lazyCopy
        
        if (!(paddings instanceof Array)) {
            paddings = [paddings].arrayByRepeating(views.length - 1)
        }
        paddings = paddings.map(padding => this._heightNumberFromSizeNumberOrFunctionOrView(padding))
        
        if (!(absoluteHeights instanceof Array) && IS_NOT_NIL(absoluteHeights)) {
            absoluteHeights = [absoluteHeights].arrayByRepeating(views.length)
        }
        absoluteHeights = absoluteHeights.map(
            height => this._heightNumberFromSizeNumberOrFunctionOrView(height)
        )
        
        for (let i = 0; i < views.length; i++) {
            const frame = currentRectangle.rectangleWithHeight(views[i])
            
            if (IS_NOT_NIL(absoluteHeights[i])) {
                frame.height = absoluteHeights[i] as number
            }
            
            views[i].frame = frame
            frames.push(frame)
            
            const padding = (paddings[i] || 0) as number
            currentRectangle = frame.rectangleForNextRow(padding)
        }
        
        return frames
    }
    
    /**
     * Distributes views horizontally as a row, assigning frames and returning them.
     * Each view is positioned to the right of the previous one with optional padding between them.
     * @param views - Array of views to distribute
     * @param paddings - Padding between views (single value or array of values)
     * @param absoluteWidths - Optional fixed widths for views (overrides intrinsic width)
     * @param centeredOnPosition - Horizontal alignment of the row within this rectangle: 0 = left (default), 0.5 =
     *     center, 1 = right
     * @returns Array of rectangles representing the frame for each view
     */
    framesByDistributingViewsAsRow(
        views: UIView[],
        paddings: SizeNumberOrFunctionOrView | SizeNumberOrFunctionOrView[] = 0,
        absoluteWidths: SizeNumberOrFunctionOrView | SizeNumberOrFunctionOrView[] = nil,
        centeredOnPosition = 0
    ) {
        const frames: UIRectangle[] = []
        let currentRectangle = this.lazyCopy() // COW: Use lazyCopy
        
        if (!(paddings instanceof Array)) {
            paddings = [paddings].arrayByRepeating(views.length - 1)
        }
        paddings = paddings.map(padding => this._widthNumberFromSizeNumberOrFunctionOrView(padding))
        
        if (!(absoluteWidths instanceof Array) && IS_NOT_NIL(absoluteWidths)) {
            absoluteWidths = [absoluteWidths].arrayByRepeating(views.length)
        }
        absoluteWidths = absoluteWidths.map(
            width => this._widthNumberFromSizeNumberOrFunctionOrView(width)
        )
        
        for (let i = 0; i < views.length; i++) {
            const frame = currentRectangle.rectangleWithWidth(views[i])
            
            if (IS_NOT_NIL(absoluteWidths[i])) {
                frame.width = absoluteWidths[i] as number
            }
            
            frames.push(frame)
            
            const padding = (paddings[i] || 0) as number
            currentRectangle = frame.rectangleForNextColumn(padding)
        }
        
        if (centeredOnPosition !== 0 && frames.length > 0) {
            const rowWidth = frames.lastElement.max.x - frames.firstElement.x
            const offset = (this.width - rowWidth) * centeredOnPosition - (frames.firstElement.x - this.x)
            frames.forEach(frame => { frame.x += offset })
        }
        
        frames.forEach((frame, index) => views[index].frame = frame)
        
        return frames
    }
    
    /**
     * Distributes views as a grid (2D array), assigning frames and returning them.
     * The first index represents rows (vertical), the second index represents columns (horizontal).
     * Example: views[0] is the first row, views[0][0] is the first column in the first row.
     * Each row is laid out horizontally, and rows are stacked vertically.
     * @param views - 2D array where views[row][column] represents the grid structure
     * @param paddings - Vertical padding between rows (single value or array of values)
     * @param absoluteHeights - Optional fixed heights for each row (overrides intrinsic height)
     * @returns 2D array of rectangles where frames[row][column] matches views[row][column]
     */
    framesByDistributingViewsAsGrid(
        views: UIView[][],
        paddings: SizeNumberOrFunctionOrView | SizeNumberOrFunctionOrView[] = 0,
        absoluteHeights: SizeNumberOrFunctionOrView | SizeNumberOrFunctionOrView[] = nil
    ) {
        const frames: UIRectangle[][] = []
        let currentRowRectangle = this.lazyCopy() // COW: Use lazyCopy
        
        if (!(paddings instanceof Array)) {
            paddings = [paddings].arrayByRepeating(views.length - 1)
        }
        paddings = paddings.map(padding => this._heightNumberFromSizeNumberOrFunctionOrView(padding))
        
        if (!(absoluteHeights instanceof Array) && IS_NOT_NIL(absoluteHeights)) {
            absoluteHeights = [absoluteHeights].arrayByRepeating(views.length)
        }
        absoluteHeights = absoluteHeights.map(
            height => this._heightNumberFromSizeNumberOrFunctionOrView(height)
        )
        
        for (let i = 0; i < views.length; i++) {
            const rowViews = views[i]
            const rowFrames = currentRowRectangle.framesByDistributingViewsAsRow(rowViews)
            
            if (IS_NOT_NIL(absoluteHeights[i])) {
                const heightNumber = absoluteHeights[i] as number
                rowFrames.forEach((frame, j) => {
                    frame.height = heightNumber
                    rowViews[j].frame = frame
                })
            }
            
            frames.push(rowFrames)
            
            const padding = (paddings[i] || 0) as number
            const maxHeight = Math.max(...rowFrames.map(f => f.height))
            currentRowRectangle = currentRowRectangle.rectangleForNextRow(padding, maxHeight)
        }
        
        return frames
    }
    
    rectangleWithIntrinsicContentSizeForView(view: UIView, centeredOnXPosition = 0, centeredOnYPosition = 0) {
        const intrinsicContentSize = view.intrinsicContentSize()
        return this.rectangleWithHeight(intrinsicContentSize.height, centeredOnYPosition)
            .rectangleWithWidth(intrinsicContentSize.width, centeredOnXPosition)
    }
    
    settingMinHeight(minHeight?: number) {
        this.minHeight = minHeight
        return this
    }
    
    settingMinWidth(minWidth?: number) {
        this.minWidth = minWidth
        return this
    }
    
    settingMaxHeight(maxHeight?: number) {
        this.maxHeight = maxHeight
        return this
    }
    
    settingMaxWidth(maxWidth?: number) {
        this.maxWidth = maxWidth
        return this
    }
    
    rectangleByEnforcingMinAndMaxSizes(centeredOnXPosition = 0, centeredOnYPosition = 0) {
        return this.rectangleWithHeight(
            [
                [this.height, this.maxHeight].filter(value => IS_NOT_LIKE_NULL(value)).min(),
                this.minHeight
            ].filter(value => IS_NOT_LIKE_NULL(value)).max(),
            centeredOnYPosition
        ).rectangleWithWidth(
            [
                [this.width, this.maxWidth].filter(value => IS_NOT_LIKE_NULL(value)).min(),
                this.minWidth
            ].filter(value => IS_NOT_LIKE_NULL(value)).max(),
            centeredOnXPosition
        )
    }
    
    
    assignedAsFrameOfView(view: UIView, isWeakFrame = view.hasWeakFrame) {
        view.frame = this
        view.hasWeakFrame = isWeakFrame
        return this
    }
    
    
    override toString() {
        
        const result = "[" + this.class.name + "] { x: " + this.x + ", y: " + this.y + ", " +
            "height: " + this.height.toFixed(2) + ", width: " + this.height.toFixed(2) + " }"
        
        return result
        
    }
    
    get [Symbol.toStringTag]() {
        return this.toString()
    }
    
    
    IF(condition: boolean): UIRectangleConditionalChain<UIRectangle> {
        const conditionalBlock = new UIRectangleConditionalBlock(this, condition)
        // @ts-ignore
        return conditionalBlock.getProxy()
    }
    
    // These will be intercepted by the proxy, but we define them for TypeScript
    ELSE_IF(condition: boolean): UIRectangle {
        return this
    }
    
    ELSE(): UIRectangle {
        return this
    }
    
    ENDIF(): this
    ENDIF<T, R>(performFunction: (result: T) => R): R
    ENDIF<T, R>(performFunction?: (result: T) => R): R | this {
        if (performFunction) {
            return performFunction(this as any)
        }
        return this
    }
    
    
    // Bounding box
    static boundingBoxForPoints(points: UIPoint[]) {
        if (points.length === 0) {
            return new UIRectangle()
        }
        
        const first = points[0]
        const result = new UIRectangle(first.x, first.y, 0, 0)
        
        for (let i = 1; i < points.length; i++) {
            result.updateByAddingPoint(points[i])
        }
        return result
    }
    
    static boundingBoxForRectanglesAndPoints(rectanglesAndPoints: (UIPoint | UIRectangle)[]) {
        if (rectanglesAndPoints.length === 0) {
            return new UIRectangle()
        }
        
        const first = rectanglesAndPoints[0]
        const result = first instanceof UIRectangle
                       ? new UIRectangle(first.x, first.y, first.height, first.width)
                       : new UIRectangle(first.x, first.y, 0, 0)
        
        for (let i = 1; i < rectanglesAndPoints.length; i++) {
            const rectangleOrPoint = rectanglesAndPoints[i]
            if (rectangleOrPoint instanceof UIRectangle) {
                result.updateByAddingPoint(rectangleOrPoint.min)
                result.updateByAddingPoint(rectangleOrPoint.max)
            }
            else {
                result.updateByAddingPoint(rectangleOrPoint)
            }
        }
        return result
    }
    
    
    beginUpdates() {
        this._isBeingUpdated = YES
    }
    
    finishUpdates() {
        this._isBeingUpdated = NO
        this.didChange()
    }
    
    
    didChange() {
        
        // Callback to be set by delegate
        
    }
    
    _rectanglePointDidChange() {
        if (!this._isBeingUpdated) {
            this.didChange()
        }
    }
    
    
}


// 1. Methods available when holding a UIRectangle
type RectangleChainMethods<TResult> = {
    [K in keyof UIRectangle as (
        K extends 'IF' | 'ELSE' | 'ELSE_IF' | 'ENDIF' ? never : K
        )]:
    UIRectangle[K] extends (...args: infer Args) => infer R
    ? R extends UIRectangle | UIRectangle[]
        // CHANGE: We do NOT add 'R' to 'TResult' here. We only update the current state (R).
      ? (...args: Args) => UIRectangleConditionalChain<R, TResult>
      : never
    : never
};

// 2. Methods available when holding a UIRectangle[]
type ArrayChainMethods<TResult> = {
    [K in keyof UIRectangle[]]:
    UIRectangle[][K] extends UIRectangle
    ? UIRectangleConditionalChain<UIRectangle, TResult> // No accumulation for properties
    : UIRectangle[][K] extends (...args: infer Args) => infer R
      ? R extends UIRectangle | UIRectangle[]
          // CHANGE: We do NOT add 'R' to 'TResult' here either.
        ? (...args: Args) => UIRectangleConditionalChain<R, TResult>
        : never
      : never
};

// 3. Methods available in both states (Control Flow + Transform)
type SharedChainMethods<TCurrent, TResult> = {
    // IF opens a nested conditional block. After the matching ENDIF(), the chain resumes
    // as a UIRectangle — both at runtime (the proxy forwards to the current rectangle) and
    // at the type level. Nesting is supported to arbitrary depth.
    IF(condition: boolean): UIRectangleConditionalChain<UIRectangle, TResult>;
    
    // TRANSFORM applies an inline function and continues the chain.
    TRANSFORM<R extends UIRectangle>(fn: (current: TCurrent) => R): UIRectangleConditionalChain<R, TResult>;
    
    // ELSE_IF / ELSE reset the branch state.
    ELSE_IF(condition: boolean): UIRectangleConditionalChain<UIRectangle, TResult | TCurrent>;
    ELSE(): UIRectangleConditionalChain<UIRectangle, TResult | TCurrent>;
    
    // ENDIF closes this IF block.
    // No-arg: always typed as UIRectangle so rectangle methods are available immediately
    // after. At runtime the proxy wraps the current rectangle and forwards all calls.
    // With transform fn: returns R directly, escaping the chain entirely.
    ENDIF(): UIRectangle;
    ENDIF<R>(performFunction: (result: TResult | TCurrent) => R): R;
};

// 4. The Main Type (No changes needed here, just re-stating for context)
type UIRectangleConditionalChain<TCurrent, TResult = TCurrent> =
    (TCurrent extends UIRectangle ? RectangleChainMethods<TResult> : {}) &
    (TCurrent extends UIRectangle[] ? ArrayChainMethods<TResult> : {}) &
    SharedChainMethods<TCurrent, TResult>;


interface UIRectangleConditionalFrame {
    // The result to resume from when this frame's ENDIF is reached (if no branch matched)
    resultBeforeIF: any
    // The result accumulated inside the active branch of this frame
    currentResult: any
    // The result at the point of IF — used to reset currentResult when entering each new branch
    originalResult: any
    // Whether any branch of this frame has already been taken (latches to true, never resets)
    anyConditionMet: boolean
    // Whether the current branch is the one that was taken (flips per ELSE_IF / ELSE)
    currentBranchActive: boolean
}

class UIRectangleConditionalBlock {
    
    // Stack of nested IF frames. The top of the stack is the innermost active IF.
    private _stack: UIRectangleConditionalFrame[]
    
    constructor(initialResult: UIRectangle, condition: boolean) {
        // Seed the stack with the first IF frame.
        // resultBeforeIF is null here because this is the outermost block;
        // ENDIF on the last frame simply returns currentResult.
        this._stack = [{
            resultBeforeIF: null,
            currentResult: initialResult,
            originalResult: initialResult,
            anyConditionMet: condition,
            currentBranchActive: condition,
        }]
    }
    
    // Convenience getter that operates on the innermost frame.
    private get _top(): UIRectangleConditionalFrame {
        return this._stack[this._stack.length - 1]
    }
    
    // A method body should only execute when every frame in the stack has its
    // current branch active (handles nested IFs correctly).
    private get _shouldExecute(): boolean {
        return this._stack.every(frame => frame.currentBranchActive)
    }
    
    private createProxy(): UIRectangleConditionalChain<any, any> {
        const self = this
        
        return new Proxy({}, {
            get(_, prop) {
                
                // ── Control Flow ────────────────────────────────────────────────────
                
                if (prop === 'IF') {
                    return (condition: boolean) => {
                        // Push a new frame. The new frame's result starts as a copy of
                        // the current innermost result so that chaining inside the nested
                        // IF begins from the right value.
                        self._stack.push({
                            resultBeforeIF: self._top.currentResult,
                            currentResult: self._top.currentResult,
                            originalResult: self._top.currentResult,
                            anyConditionMet: condition,
                            currentBranchActive: condition,
                        })
                        return self.createProxy()
                    }
                }
                
                if (prop === 'TRANSFORM') {
                    return <R extends UIRectangle>(fn: (current: any) => R) => {
                        if (self._shouldExecute) {
                            self._top.currentResult = fn(self._top.currentResult)
                        }
                        return self.createProxy()
                    }
                }
                
                if (prop === 'ELSE_IF') {
                    return (condition: boolean) => {
                        const top = self._top
                        // Only consider this branch if no prior branch has been taken.
                        // Always deactivate the current branch first, then activate only
                        // if this condition is true and nothing has matched yet.
                        top.currentBranchActive = !top.anyConditionMet && condition
                        if (top.currentBranchActive) {
                            top.anyConditionMet = true
                            top.currentResult = top.originalResult
                        }
                        return self.createProxy()
                    }
                }
                
                if (prop === 'ELSE') {
                    return () => {
                        const top = self._top
                        top.currentBranchActive = !top.anyConditionMet
                        if (top.currentBranchActive) {
                            top.anyConditionMet = true
                            top.currentResult = top.originalResult
                        }
                        return self.createProxy()
                    }
                }
                
                if (prop === 'ENDIF') {
                    function endif(): any
                    function endif<R>(performFunction: (result: any) => R): R
                    function endif<R>(performFunction?: (result: any) => R): R | any {
                        
                        if (self._stack.length === 1) {
                            // Outermost ENDIF. Return the bare rectangle (or transform it).
                            const top = self._top
                            const result = top.currentResult
                            if (performFunction && top.anyConditionMet) {
                                return performFunction(result)
                            }
                            else {
                                return result
                            }
                        }
                        
                        // Pop the innermost (nested) frame.
                        const completedFrame = self._stack.pop()!
                        
                        // If any branch was taken use its accumulated result; otherwise
                        // fall back to the value that existed before entering this IF.
                        const resolvedResult = completedFrame.anyConditionMet
                                               ? completedFrame.currentResult
                                               : completedFrame.resultBeforeIF
                        
                        // Optionally transform, then write back into the parent frame.
                        const finalResult = performFunction ? performFunction(resolvedResult) : resolvedResult
                        self._top.currentResult = finalResult
                        
                        // Return the proxy so the outer chain can continue.
                        return self.createProxy()
                    }
                    return endif
                }
                
                // ── Forward to currentResult (UIRectangle or UIRectangle[]) ─────────
                
                const value = self._top.currentResult[prop]
                
                // Case A: method call
                if (typeof value === 'function') {
                    return (...args: any[]) => {
                        if (self._shouldExecute) {
                            self._top.currentResult = value.apply(self._top.currentResult, args)
                        }
                        return self.createProxy()
                    }
                }
                
                // Case B: property access (e.g. array .lastElement)
                if (self._shouldExecute) {
                    self._top.currentResult = value
                }
                
                return self.createProxy()
            }
        }) as any
    }
    
    getProxy(): UIRectangleConditionalChain<any, any> {
        return this.createProxy()
    }
    
}

