import { Sizeable } from "./types";
import { BBox } from "../util/bbox";
import { isNumber } from "../util/types";
const { min, max, round } = Math;
export class Layoutable {
    constructor() {
        this.absolute = false;
        this._bbox = new BBox();
        this._inner_bbox = new BBox();
        this._dirty = false;
        this._handlers = [];
    }
    *[Symbol.iterator]() { }
    get bbox() {
        return this._bbox;
    }
    get inner_bbox() {
        return this._inner_bbox;
    }
    get sizing() {
        return this._sizing;
    }
    get visible() {
        return this._sizing.visible;
    }
    set visible(visible) {
        if (this._sizing.visible != visible) {
            this._sizing.visible = visible;
            this._dirty = true;
        }
    }
    set_sizing(sizing = {}) {
        const width_policy = sizing.width_policy ?? "fit";
        const width = sizing.width;
        const min_width = sizing.min_width;
        const max_width = sizing.max_width;
        const height_policy = sizing.height_policy ?? "fit";
        const height = sizing.height;
        const min_height = sizing.min_height;
        const max_height = sizing.max_height;
        const aspect = sizing.aspect;
        const margin = sizing.margin ?? { top: 0, right: 0, bottom: 0, left: 0 };
        const visible = sizing.visible ?? true;
        const halign = sizing.halign ?? "start";
        const valign = sizing.valign ?? "start";
        this._sizing = {
            width_policy, min_width, width, max_width,
            height_policy, min_height, height, max_height,
            aspect,
            margin,
            visible,
            halign,
            valign,
            size: { width, height },
        };
        this._init();
    }
    _init() { }
    _set_geometry(outer, inner) {
        this._bbox = outer;
        this._inner_bbox = inner;
    }
    set_geometry(outer, inner) {
        const { fixup_geometry } = this;
        if (fixup_geometry != null) {
            [outer, inner] = fixup_geometry(outer, inner);
        }
        this._set_geometry(outer, inner ?? outer);
        for (const handler of this._handlers) {
            handler(this._bbox, this._inner_bbox);
        }
    }
    on_resize(handler) {
        this._handlers.push(handler);
    }
    is_width_expanding() {
        return this.sizing.width_policy == "max";
    }
    is_height_expanding() {
        return this.sizing.height_policy == "max";
    }
    apply_aspect(viewport, { width, height }) {
        const { aspect } = this.sizing;
        if (aspect != null) {
            const { width_policy, height_policy } = this.sizing;
            const gt = (width, height) => {
                const policies = { max: 4, fit: 3, min: 2, fixed: 1 };
                return policies[width] > policies[height];
            };
            if (width_policy != "fixed" && height_policy != "fixed") {
                if (width_policy == height_policy) {
                    const w_width = width;
                    const w_height = round(width / aspect);
                    const h_width = round(height * aspect);
                    const h_height = height;
                    const w_diff = Math.abs(viewport.width - w_width) + Math.abs(viewport.height - w_height);
                    const h_diff = Math.abs(viewport.width - h_width) + Math.abs(viewport.height - h_height);
                    if (w_diff <= h_diff) {
                        width = w_width;
                        height = w_height;
                    }
                    else {
                        width = h_width;
                        height = h_height;
                    }
                }
                else if (gt(width_policy, height_policy)) {
                    height = round(width / aspect);
                }
                else {
                    width = round(height * aspect);
                }
            }
            else if (width_policy == "fixed") {
                height = round(width / aspect);
            }
            else if (height_policy == "fixed") {
                width = round(height * aspect);
            }
        }
        return { width, height };
    }
    measure(viewport_size) {
        if (!this.sizing.visible)
            return { width: 0, height: 0 };
        const exact_width = (width) => {
            return this.sizing.width_policy == "fixed" && this.sizing.width != null ? this.sizing.width : width;
        };
        const exact_height = (height) => {
            return this.sizing.height_policy == "fixed" && this.sizing.height != null ? this.sizing.height : height;
        };
        const viewport = new Sizeable(viewport_size)
            .shrink_by(this.sizing.margin)
            .map(exact_width, exact_height);
        const computed = this._measure(viewport);
        const clipped = this.clip_size(computed, viewport);
        const width = exact_width(clipped.width);
        const height = exact_height(clipped.height);
        const size = this.apply_aspect(viewport, { width, height });
        return { ...computed, ...size };
    }
    compute(viewport = {}) {
        const size_hint = this.measure({
            width: viewport.width != null && this.is_width_expanding() ? viewport.width : Infinity,
            height: viewport.height != null && this.is_height_expanding() ? viewport.height : Infinity,
        });
        const { width, height } = size_hint;
        const outer = new BBox({ left: 0, top: 0, width, height });
        let inner = undefined;
        if (size_hint.inner != null) {
            const { left, top, right, bottom } = size_hint.inner;
            inner = new BBox({ left, top, right: width - right, bottom: height - bottom });
        }
        this.set_geometry(outer, inner);
    }
    get xview() {
        return this.bbox.xview;
    }
    get yview() {
        return this.bbox.yview;
    }
    clip_size(size, viewport) {
        function clip(size, vsize, min_size, max_size) {
            if (min_size == null)
                min_size = 0;
            else if (!isNumber(min_size))
                min_size = Math.round(min_size.percent * vsize);
            if (max_size == null)
                max_size = Infinity;
            else if (!isNumber(max_size))
                max_size = Math.round(max_size.percent * vsize);
            return max(min_size, min(size, max_size));
        }
        return {
            width: clip(size.width, viewport.width, this.sizing.min_width, this.sizing.max_width),
            height: clip(size.height, viewport.height, this.sizing.min_height, this.sizing.max_height),
        };
    }
    has_size_changed() {
        const { _dirty } = this;
        this._dirty = false;
        return _dirty;
    }
}
Layoutable.__name__ = "Layoutable";
export class ContentLayoutable extends Layoutable {
    _measure(viewport) {
        const content_size = this._content_size();
        const bounds = viewport
            .bounded_to(this.sizing.size)
            .bounded_to(content_size);
        const width = (() => {
            switch (this.sizing.width_policy) {
                case "fixed":
                    return this.sizing.width != null ? this.sizing.width : content_size.width;
                case "min":
                    return content_size.width;
                case "fit":
                    return bounds.width;
                case "max":
                    return Math.max(content_size.width, bounds.width);
            }
        })();
        const height = (() => {
            switch (this.sizing.height_policy) {
                case "fixed":
                    return this.sizing.height != null ? this.sizing.height : content_size.height;
                case "min":
                    return content_size.height;
                case "fit":
                    return bounds.height;
                case "max":
                    return Math.max(content_size.height, bounds.height);
            }
        })();
        return { width, height };
    }
}
ContentLayoutable.__name__ = "ContentLayoutable";
//# sourceMappingURL=layoutable.js.map