/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Views
 */
import { assert, CompressedId64Set, dispose } from "@itwin/core-bentley";
import { Angle, ClipShape, ClipVector, Constant, Matrix3d, Point2d, Point3d, PolyfaceBuilder, Range2d, Range3d, StrokeOptions, Transform } from "@itwin/core-geometry";
import { ColorDef, Feature, FeatureTable, Frustum, Gradient, GraphicParams, PackedFeatureTable, Placement2d, TextureTransparency, } from "@itwin/core-common";
import { CategorySelectorState } from "./CategorySelectorState";
import { DisplayStyle2dState } from "./DisplayStyleState";
import { GraphicBranch } from "./render/GraphicBranch";
import { Frustum2d } from "./Frustum2d";
import { MockRender } from "./render/MockRender";
import { FeatureSymbology } from "./render/FeatureSymbology";
import { IModelApp } from "./IModelApp";
import { CoordSystem } from "./CoordSystem";
import { OffScreenViewport } from "./Viewport";
import { ViewState2d } from "./ViewState";
import { createDefaultViewFlagOverrides, TileGraphicType } from "./tile/internal";
import { imageBufferToPngDataUrl, openImageDataUrlInNewWindow } from "./common/ImageUtil";
import { ViewRect } from "./common/ViewRect";
import { GraphicType } from "./common/render/GraphicType";
// cSpell:ignore ovrs
/** Describes the geometry and styling of a sheet border decoration.
 * The sheet border decoration mimics a sheet of paper with a drop shadow.
 */
class SheetBorder {
    constructor(rect, shadow, gradient) {
        this._rect = rect;
        this._shadow = shadow;
        this._gradient = gradient;
    }
    /** Create a new sheet border. If a context is supplied, points are transformed to view coordinates. */
    static create(width, height, context) {
        // Rect
        const rect = [
            Point3d.create(0, height),
            Point3d.create(0, 0),
            Point3d.create(width, 0),
            Point3d.create(width, height),
            Point3d.create(0, height)
        ];
        if (context) {
            context.viewport.worldToViewArray(rect);
        }
        // Shadow
        const shadowWidth = .01 * Math.sqrt(width * width + height * height);
        const shadow = [
            Point3d.create(shadowWidth, 0),
            Point3d.create(shadowWidth, -shadowWidth),
            Point3d.create(width + shadowWidth, -shadowWidth),
            Point3d.create(width + shadowWidth, height - shadowWidth),
            Point3d.create(width, height - shadowWidth),
            Point3d.create(width, 0),
            Point3d.create(shadowWidth, 0),
        ];
        if (context) {
            context.viewport.worldToViewArray(shadow);
        }
        // Gradient
        const gradient = new Gradient.Symb();
        gradient.mode = Gradient.Mode.Linear;
        gradient.angle = Angle.createDegrees(-45);
        gradient.keys = [{ value: 0, color: ColorDef.from(25, 25, 25) }, { value: 0.5, color: ColorDef.from(150, 150, 150) }];
        // Copy over points
        const rect2d = [];
        for (const point of rect)
            rect2d.push(Point2d.createFrom(point));
        const shadow2d = [];
        for (const point of shadow)
            shadow2d.push(Point2d.createFrom(point));
        return new SheetBorder(rect2d, shadow2d, gradient);
    }
    getRange() {
        const range = Range2d.createArray(this._rect);
        const shadowRange = Range2d.createArray(this._shadow);
        range.extendRange(shadowRange);
        return range;
    }
    /** Add this border to the given GraphicBuilder. */
    addToBuilder(builder) {
        const lineColor = ColorDef.black;
        const fillColor = ColorDef.black;
        const params = new GraphicParams();
        params.fillColor = fillColor;
        params.gradient = this._gradient;
        builder.activateGraphicParams(params);
        builder.addShape2d(this._shadow, Frustum2d.minimumZDistance);
        builder.setSymbology(lineColor, fillColor, 2);
        builder.addLineString2d(this._rect, 0);
    }
}
/** The information required to instantiate an ViewAttachments object to draw ViewAttachments into a sheet. The list of view attachment Ids is
 * supplied to SheetViewState via the constructor. The corresponding ViewAttachmentProps for each attachment are obtained asynchronously in
 * SheetViewState.load(). The Attachments object is created in SheetViewState.attachToViewport and disposed of in SheetViewState.detachFromViewport.
 */
class ViewAttachmentsInfo {
    get attachments() { return this._attachments; }
    constructor(attachments) {
        this._attachments = attachments;
    }
    get isLoaded() {
        return 0 === this._attachments.length || "string" !== typeof this._attachments[0];
    }
    get viewAttachmentProps() {
        return this.isLoaded ? this._props : [];
    }
    get _props() {
        assert(this.isLoaded);
        return this._attachments;
    }
    get _ids() {
        assert(!this.isLoaded);
        return this._attachments;
    }
    static fromJSON(ids = []) {
        return new ViewAttachmentsInfo(ids);
    }
    toJSON() {
        return this.isLoaded ? this._props.map((x) => x.id) : [...this._ids];
    }
    clone(iModel) {
        let attachments = this._attachments;
        if (this.isLoaded) {
            // Need to clone the attached ViewStates.
            attachments = attachments.map((attachment) => {
                assert(typeof attachment !== "string");
                return {
                    ...attachment,
                    attachedView: attachment.attachedView.clone(iModel),
                };
            });
        }
        return new ViewAttachmentsInfo(attachments);
    }
    preload(options) {
        if (this.isLoaded)
            return;
        options.sheetViewAttachmentIds = CompressedId64Set.sortAndCompress(this._ids);
        options.viewStateLoadProps = {
            displayStyle: {
                omitScheduleScriptElementIds: !IModelApp.tileAdmin.enableFrontendScheduleScripts,
                compressExcludedElementIds: true,
            },
        };
    }
    async postload(options, iModel) {
        if (options.sheetViewViews === undefined)
            return;
        if (options.sheetViewAttachmentProps === undefined)
            return;
        const viewStateProps = options.sheetViewViews; // This is viewstateProps, need to turn this into ViewState
        const promises = [];
        for (const viewProps of viewStateProps) {
            const loadView = async () => {
                try {
                    if (viewProps === undefined)
                        return undefined;
                    const view = await iModel.views.convertViewStatePropsToViewState(viewProps);
                    return view;
                }
                catch {
                    return undefined;
                }
            };
            promises.push(loadView());
        }
        const views = await Promise.all(promises);
        const attachmentProps = options.sheetViewAttachmentProps;
        assert(views.length === attachmentProps.length);
        const attachments = [];
        for (let i = 0; i < views.length; i++) {
            const view = views[i];
            if (view && !(view instanceof SheetViewState)) {
                const props = attachmentProps[i];
                props.attachedView = view;
                attachments.push(props);
            }
        }
        this._attachments = attachments;
    }
    async load(iModel) {
        if (this.isLoaded)
            return;
        const attachmentProps = await iModel.elements.getProps(this._ids);
        const promises = [];
        for (const attachment of attachmentProps) {
            const loadView = async () => {
                try {
                    const view = await iModel.views.load(attachment.view.id);
                    return view;
                }
                catch {
                    return undefined;
                }
            };
            promises.push(loadView());
        }
        const views = await Promise.all(promises);
        assert(views.length === attachmentProps.length);
        const attachments = [];
        for (let i = 0; i < views.length; i++) {
            const view = views[i];
            if (view && !(view instanceof SheetViewState)) {
                const props = attachmentProps[i];
                props.attachedView = view;
                attachments.push(props);
            }
        }
        this._attachments = attachments;
    }
    createAttachments(sheetView) {
        return this.isLoaded ? new ViewAttachments(this._props, sheetView) : undefined;
    }
}
/** The set of view attachments to be displayed in a Viewport via a SheetViewState. Allocated when the view becomes attached to a Viewport;
 * disposed of when it becomes detached from the viewport.
 */
class ViewAttachments {
    constructor(infos, sheetView) {
        this._attachments = [];
        this.maxDepth = Frustum2d.minimumZDistance;
        for (const info of infos) {
            const drawAsRaster = info.jsonProperties?.displayOptions?.drawAsRaster || (info.attachedView.is3d() && info.attachedView.isCameraOn);
            const ctor = drawAsRaster ? RasterAttachment : OrthographicAttachment;
            const attachment = new ctor(info.attachedView, info, sheetView);
            this._attachments.push(attachment);
            this.maxDepth = Math.max(this.maxDepth, attachment.zDepth);
        }
    }
    dispose() {
        for (const attachment of this._attachments)
            attachment.dispose();
        this._attachments.length = 0;
    }
    [Symbol.iterator]() {
        return this._attachments[Symbol.iterator]();
    }
    /** For tests. */
    get attachments() {
        return this._attachments;
    }
    get isEmpty() {
        return 0 === this._attachments.length;
    }
    get areAllTileTreesLoaded() {
        return this._attachments.every((x) => x.areAllTileTreesLoaded);
    }
    discloseTileTrees(trees) {
        for (const attachment of this._attachments)
            trees.disclose(attachment);
    }
    collectStatistics(stats) {
        for (const attachment of this._attachments)
            attachment.collectStatistics(stats);
    }
    addToScene(context) {
        for (const attachment of this._attachments)
            attachment.addToScene(context);
    }
    findById(attachmentId) {
        return this._attachments.find((attachment) => attachment.viewAttachmentProps.id === attachmentId);
    }
}
/** A view of a [SheetModel]($backend).
 * @public
 * @extensions
 */
export class SheetViewState extends ViewState2d {
    get attachmentIds() {
        return this._attachmentsInfo.toJSON();
    }
    static get className() { return "SheetViewDefinition"; }
    static createFromProps(viewStateData, iModel) {
        const cat = new CategorySelectorState(viewStateData.categorySelectorProps, iModel);
        const displayStyleState = new DisplayStyle2dState(viewStateData.displayStyleProps, iModel);
        // use "new this" so subclasses are correct
        return new this(viewStateData.viewDefinitionProps, iModel, cat, displayStyleState, viewStateData.sheetProps, viewStateData.sheetAttachments);
    }
    toProps() {
        const props = super.toProps();
        props.sheetAttachments = this._attachmentsInfo.toJSON();
        // For sheetProps all that is actually used is the size, so just null out everything else.
        const codeProps = { spec: "", scope: "", value: "" };
        props.sheetProps = {
            model: "",
            code: codeProps,
            classFullName: "",
            width: this.sheetSize.x,
            height: this.sheetSize.y,
            scale: 1,
        };
        return props;
    }
    /** Strictly for testing. @internal */
    get viewAttachmentProps() {
        return this._attachmentsInfo.viewAttachmentProps.map((x) => {
            return {
                ...x,
                attachedView: undefined,
            };
        });
    }
    /** Strictly for testing. @internal */
    get viewAttachmentInfos() {
        return this._attachmentsInfo.attachments;
    }
    /** Strictly for testing. @internal */
    get attachments() {
        return this._attachments?.attachments;
    }
    isDrawingView() { return false; }
    isSheetView() { return true; }
    constructor(props, iModel, categories, displayStyle, sheetProps, attachments) {
        super(props, iModel, categories, displayStyle);
        if (categories instanceof SheetViewState) {
            // we are coming from clone...
            this.sheetSize = categories.sheetSize.clone();
            this._attachmentsInfo = categories._attachmentsInfo.clone(iModel);
            this._viewedExtents = categories._viewedExtents.clone();
        }
        else {
            this.sheetSize = Point2d.create(sheetProps.width, sheetProps.height);
            this._attachmentsInfo = ViewAttachmentsInfo.fromJSON(attachments);
            const extents = new Range3d(0, 0, 0, this.sheetSize.x, this.sheetSize.y, 0);
            const margin = 1.1;
            extents.scaleAboutCenterInPlace(margin);
            this._viewedExtents = extents;
        }
    }
    getOrigin() {
        const origin = super.getOrigin();
        if (this._attachments)
            origin.z = -this._attachments.maxDepth;
        return origin;
    }
    getExtents() {
        const extents = super.getExtents();
        if (this._attachments)
            extents.z = this._attachments.maxDepth + Frustum2d.minimumZDistance;
        return extents;
    }
    /** Overrides [[ViewState.discloseTileTrees]] to include tile trees associated with [ViewAttachment]($backend)s displayed on this sheet. */
    discloseTileTrees(trees) {
        super.discloseTileTrees(trees);
        if (this._attachments)
            trees.disclose(this._attachments);
    }
    /** @internal */
    collectNonTileTreeStatistics(stats) {
        super.collectNonTileTreeStatistics(stats);
        if (this._attachments)
            this._attachments.collectStatistics(stats);
    }
    get defaultExtentLimits() {
        return { min: Constant.oneMillimeter, max: this.sheetSize.magnitude() * 10 };
    }
    getViewedExtents() {
        return this._viewedExtents;
    }
    /** @internal */
    preload(hydrateRequest) {
        super.preload(hydrateRequest);
        this._attachmentsInfo.preload(hydrateRequest);
    }
    /** @internal */
    async postload(hydrateResponse) {
        const promises = [];
        promises.push(super.postload(hydrateResponse));
        promises.push(this._attachmentsInfo.postload(hydrateResponse, this.iModel));
        await Promise.all(promises);
    }
    /** @internal */
    createScene(context) {
        super.createScene(context);
        if (this._attachments)
            this._attachments.addToScene(context);
    }
    /** @internal */
    get secondaryViewports() {
        const attachments = this._attachments;
        if (!attachments)
            return super.secondaryViewports;
        function* iterator() {
            for (const attachment of attachments) {
                const vp = attachment.viewport;
                if (vp)
                    yield vp;
            }
        }
        return {
            [Symbol.iterator]: () => iterator(),
        };
    }
    /** @internal */
    async queryAttachmentIds() {
        const ecsql = `SELECT ECInstanceId as attachmentId FROM bis.ViewAttachment WHERE model.Id=${this.baseModelId}`;
        const ids = [];
        for await (const row of this.iModel.createQueryReader(ecsql))
            ids.push(row[0]);
        return ids;
    }
    async changeViewedModel(modelId) {
        await super.changeViewedModel(modelId);
        const attachmentIds = await this.queryAttachmentIds();
        this._attachmentsInfo = ViewAttachmentsInfo.fromJSON(attachmentIds);
        assert(undefined === this._attachments);
    }
    /** See [[ViewState.attachToViewport]]. */
    attachToViewport(args) {
        super.attachToViewport(args);
        assert(undefined === this._attachments);
        this._attachments = this._attachmentsInfo.createAttachments(this);
    }
    /** See [[ViewState.detachFromViewport]]. */
    detachFromViewport() {
        super.detachFromViewport();
        this._attachments = dispose(this._attachments);
    }
    get areAllTileTreesLoaded() {
        return super.areAllTileTreesLoaded && (!this._attachments || this._attachments.areAllTileTreesLoaded);
    }
    /** Create a sheet border decoration graphic. */
    createBorder(width, height, context) {
        const border = SheetBorder.create(width, height, context);
        const builder = context.createGraphicBuilder(GraphicType.ViewBackground);
        border.addToBuilder(builder);
        return builder.finish();
    }
    /** @internal */
    decorate(context) {
        super.decorate(context);
        if (this.sheetSize !== undefined) {
            const border = this.createBorder(this.sheetSize.x, this.sheetSize.y, context);
            context.setViewBackground(border);
        }
    }
    computeFitRange() {
        const size = this.sheetSize;
        if (0 >= size.x || 0 >= size.y)
            return super.computeFitRange();
        return new Range3d(0, 0, -1, size.x, size.y, 1);
    }
    /** @internal */
    getAttachmentViewport(args) {
        const attachment = args.viewAttachmentId ? this._attachments?.findById(args.viewAttachmentId) : undefined;
        if (!attachment) {
            return undefined;
        }
        return args.inSectionDrawingAttachment ? attachment.viewport?.view.getAttachmentViewport({ inSectionDrawingAttachment: true }) : attachment.viewport;
    }
    /** @beta */
    computeDisplayTransform(args) {
        // ###TODO we're currently ignoring model and element Id in args, assuming irrelevant for sheets.
        // Should probably call super or have super call us.
        const attachment = undefined !== args.viewAttachmentId ? this._attachments?.findById(args.viewAttachmentId) : undefined;
        if (!attachment || !(attachment instanceof OrthographicAttachment)) {
            return undefined;
        }
        const sheetTransform = attachment.toSheet;
        const sectionTransform = args.inSectionDrawingAttachment ? attachment.view.computeDisplayTransform(args) : undefined;
        if (!sectionTransform) {
            return sheetTransform.clone(args.output);
        }
        return sheetTransform.multiplyTransformTransform(sectionTransform, args.output);
    }
}
/** A mostly no-op RenderTarget for an Attachment.
 * its Scene and symbology overrides.
 */
class AttachmentTarget extends MockRender.OffScreenTarget {
    constructor(attachment) {
        // The dimensions don't matter - we're not drawing anything.
        const rect = new ViewRect(1, 1);
        super(IModelApp.renderSystem, rect);
        this._attachment = attachment;
    }
    changeScene(scene) {
        this._attachment.scene = scene;
    }
    overrideFeatureSymbology(ovrs) {
        this._attachment.symbologyOverrides = ovrs;
    }
}
/** Draws the contents a 2d or orthographic 3d view directly into a sheet view.
 * We select tiles for the view in the context of a light-weight offscreen viewport with a no-op RenderTarget, then
 * collect the resultant graphics and add them to the sheet view's scene.
 */
class OrthographicAttachment {
    get view() {
        return this._viewport.view;
    }
    get viewAttachmentProps() {
        return this._props;
    }
    get viewport() {
        return this._viewport;
    }
    constructor(view, props, sheetView) {
        this._viewRect = new ViewRect(0, 0, 1, 1);
        this._originalFrustum = new Frustum();
        this.symbologyOverrides = new FeatureSymbology.Overrides(view);
        const target = new AttachmentTarget(this);
        this._viewport = OffScreenViewport.createViewport(view, target, true);
        this._props = props;
        this._sheetModelId = sheetView.baseModelId;
        const applyClip = true; // set to false for debugging
        this._viewFlagOverrides = {
            ...view.viewFlags,
            clipVolume: applyClip,
            lighting: false,
            shadows: false,
        };
        const placement = Placement2d.fromJSON(props.placement);
        const range = placement.calculateRange();
        this._range = range;
        this._sizeInMeters = new Point2d(range.xLength(), range.yLength());
        // Compute transform from attached view's world coordinates to sheet's world coordinates.
        // NB: We obtain the extents and origin from the *viewport* not the *view* - they may have been adjusted by the viewport.
        const applySkew = true; // set to false for debugging
        const skew = applySkew ? view.getAspectRatioSkew() : 1;
        const extents = this._viewport.viewingSpace.viewDelta.clone();
        const zDepth = Math.abs(extents.z);
        const scaleX = this._sizeInMeters.x / Math.abs(extents.x);
        const scaleY = skew * this._sizeInMeters.y / Math.abs(extents.y);
        this._scale = { x: 1 / scaleX, y: 1 / scaleY };
        const zBias = Frustum2d.depthFromDisplayPriority(props.jsonProperties?.displayPriority ?? 0);
        this.zDepth = 1.01 * (zDepth - zBias); // give a little padding so that geometry right up against far plane doesn't get clipped.
        // View origin is at the *back* of the view. Align *front* of view based on display priority.
        const viewRot = view.getRotation();
        const viewOrg = viewRot.multiplyVector(this._viewport.viewingSpace.viewOrigin);
        viewOrg.z += zDepth;
        viewRot.multiplyTransposeVectorInPlace(viewOrg);
        const matrix = Matrix3d.createScale(scaleX, scaleY, 1);
        matrix.multiplyMatrixMatrix(viewRot, matrix);
        const origin = Matrix3d.xyzMinusMatrixTimesXYZ(viewOrg, matrix, viewOrg);
        const attachmentOrigin = Point3d.createFrom(placement.origin);
        attachmentOrigin.z = zBias;
        const viewOrgToAttachment = attachmentOrigin.minus(viewOrg);
        origin.addInPlace(viewOrgToAttachment);
        this._toSheet = Transform.createRefs(origin, matrix);
        this._fromSheet = this._toSheet.inverse();
        // If the attached view is a section drawing, it may itself have an attached spatial view with a clip.
        // The clip needs to be transformed into sheet space.
        if (view.isDrawingView())
            this._viewport.drawingToSheetTransform = this._toSheet;
        // ###TODO? If we also apply the attachment's clip to the attached view, we may get additional culling during tile selection.
        // However the attached view's frustum is already clipped by intersection with sheet view's frustum, and additional clipping planes
        // introduce additional computation, so possibly not worth it.
        // Transform the view's clip (if any) to sheet space
        let viewClip = view.viewFlags.clipVolume ? view.getViewClip()?.clone() : undefined;
        if (viewClip)
            viewClip.transformInPlace(this._toSheet);
        else
            viewClip = ClipVector.createEmpty();
        let sheetClip;
        if (undefined !== props.jsonProperties?.clip)
            sheetClip = ClipVector.fromJSON(props.jsonProperties?.clip);
        if (sheetClip && sheetClip.isValid) {
            // Clip to view attachment's clip. NB: clip is in sheet coordinate space.
            for (const clip of sheetClip.clips)
                viewClip.clips.push(clip);
        }
        else {
            // Clip to view attachment's bounding box
            viewClip.appendShape([
                Point3d.create(this._range.low.x, this._range.low.y),
                Point3d.create(this._range.high.x, this._range.low.y),
                Point3d.create(this._range.high.x, this._range.high.y),
                Point3d.create(this._range.low.x, this._range.high.y),
            ]);
        }
        this._clipVolume = IModelApp.renderSystem.createClipVolume(viewClip);
        // Save off the original frustum (potentially adjusted by viewport).
        this._viewport.setupFromView();
        this._viewport.viewingSpace.getFrustum(CoordSystem.World, true, this._originalFrustum);
        const applyHiddenLineSettings = true; // for debugging edge display, set to false...
        const style = view.displayStyle;
        if (style.is3d() && applyHiddenLineSettings)
            this._hiddenLineSettings = style.settings.hiddenLineSettings;
    }
    dispose() {
        this._viewport.dispose();
    }
    discloseTileTrees(trees) {
        trees.disclose(this._viewport);
    }
    addToScene(context) {
        if (context.viewport.freezeScene)
            return;
        if (!context.viewport.view.viewsCategory(this._props.category))
            return;
        const wantBounds = context.viewport.wantViewAttachmentBoundaries;
        const wantClipShapes = context.viewport.wantViewAttachmentClipShapes;
        if (wantBounds || wantClipShapes) {
            const builder = context.createSceneGraphicBuilder();
            if (wantBounds) {
                builder.setSymbology(ColorDef.red, ColorDef.red, 2);
                builder.addRangeBox(this._range);
            }
            if (wantClipShapes && this._clipVolume) {
                builder.setSymbology(ColorDef.blue, ColorDef.blue, 2);
                for (const prim of this._clipVolume.clipVector.clips) {
                    if (!(prim instanceof ClipShape))
                        continue; // ###TODO handle non-shape primitives, if any such ever encountered
                    const pts = [];
                    const tf = prim.transformFromClip;
                    for (const pt of prim.polygon) {
                        const tfPt = tf ? tf.multiplyPoint3d(pt) : pt;
                        pts.push(new Point2d(tfPt.x, tfPt.y));
                    }
                    builder.addLineString2d(pts, 0);
                }
            }
            // Put into a Batch so that we can see tooltip with attachment Id on mouseover.
            const batch = context.target.renderSystem.createBatch(builder.finish(), this.getDebugFeatureTable(), this._range);
            context.outputGraphic(batch);
        }
        if (!context.viewport.wantViewAttachments)
            return;
        // Pixel size used to compute size of ViewRect so that tiles of appropriate LOD are selected.
        const pixelSize = context.viewport.getPixelSizeAtPoint();
        if (0 === pixelSize)
            return;
        // Adjust attached view frustum based on intersection with sheet view frustum.
        const attachFrustum = this._originalFrustum.transformBy(this._toSheet);
        const attachFrustumRange = attachFrustum.toRange();
        const sheetFrustum = context.viewport.getWorldFrustum();
        const sheetFrustumRange = sheetFrustum.toRange();
        const intersect = attachFrustumRange.intersect(sheetFrustumRange);
        if (intersect.isNull)
            return;
        attachFrustum.initFromRange(intersect);
        attachFrustum.transformBy(this._fromSheet, attachFrustum);
        this._viewport.setupViewFromFrustum(attachFrustum);
        // Adjust view rect based on size of attachment on screen so that tiles of appropriate LOD are selected.
        const width = this._sizeInMeters.x * intersect.xLength() / attachFrustumRange.xLength();
        const height = this._sizeInMeters.y * intersect.yLength() / attachFrustumRange.yLength();
        this._viewRect.width = Math.max(1, Math.round(width / pixelSize));
        this._viewRect.height = Math.max(1, Math.round(height / pixelSize));
        this._viewport.setRect(this._viewRect);
        // Propagate settings from on-screen viewport.
        this._viewport.debugBoundingBoxes = context.viewport.debugBoundingBoxes;
        this._viewport.setTileSizeModifier(context.viewport.tileSizeModifier);
        // Create the scene.
        this._viewport.renderFrame();
        const scene = this.scene;
        if (!scene)
            return;
        // Extract scene graphics and insert into on-screen scene context.
        const options = {
            viewAttachmentId: this._props.id,
            clipVolume: this._clipVolume,
            hline: this._hiddenLineSettings,
            frustum: {
                is3d: this.view.is3d(),
                scale: this._scale,
            },
        };
        const outputGraphics = (source) => {
            if (0 === source.length)
                return;
            const graphics = new GraphicBranch();
            graphics.setViewFlagOverrides(this._viewFlagOverrides);
            graphics.symbologyOverrides = this.symbologyOverrides;
            for (const graphic of source)
                graphics.entries.push(graphic);
            const branch = context.createGraphicBranch(graphics, this._toSheet, options);
            context.outputGraphic(branch);
        };
        outputGraphics(scene.foreground);
        context.withGraphicType(TileGraphicType.BackgroundMap, () => outputGraphics(scene.background));
        context.withGraphicType(TileGraphicType.Overlay, () => outputGraphics(scene.overlay));
        // Report tile statistics to sheet view's viewport.
        const tileAdmin = IModelApp.tileAdmin;
        const selectedAndReady = tileAdmin.getTilesForUser(this._viewport);
        const requested = tileAdmin.getRequestsForUser(this._viewport);
        tileAdmin.addExternalTilesForUser(context.viewport, {
            requested: requested?.size ?? 0,
            selected: selectedAndReady?.selected.size ?? 0,
            ready: selectedAndReady?.ready.size ?? 0,
        });
    }
    getDebugFeatureTable() {
        if (this._debugFeatureTable)
            return this._debugFeatureTable;
        const featureTable = new FeatureTable(1, this._sheetModelId);
        featureTable.insert(new Feature(this._props.id));
        this._debugFeatureTable = PackedFeatureTable.pack(featureTable);
        return this._debugFeatureTable;
    }
    get areAllTileTreesLoaded() {
        return this.view.areAllTileTreesLoaded;
    }
    collectStatistics(_stats) {
        // Handled by discloseTileTrees()
    }
    get toSheet() {
        return this._toSheet;
    }
}
function createRasterAttachmentViewport(_view, _rect, _attachment) {
    class RasterAttachmentViewport extends OffScreenViewport {
        constructor(view, rect, attachment) {
            super(IModelApp.renderSystem.createOffscreenTarget(rect));
            this._isSceneReady = false;
            this._attachment = attachment;
            this._isAspectRatioLocked = true;
            this.changeView(view);
        }
        createSceneContext() {
            assert(!this._isSceneReady);
            this._sceneContext = super.createSceneContext();
            return this._sceneContext;
        }
        renderFrame() {
            assert(!this._isSceneReady);
            this.clearSceneContext();
            super.renderFrame();
            if (undefined !== this._sceneContext) {
                this._isSceneReady = !this._sceneContext.hasMissingTiles && this.view.areAllTileTreesLoaded;
                if (this._isSceneReady)
                    this._attachment.produceGraphics(this._sceneContext);
                this._sceneContext = undefined;
            }
        }
        clearSceneContext() {
            this._sceneContext = undefined;
        }
        addDecorations(_decorations) {
            // ###TODO: skybox, ground plane, possibly grid. DecorateContext requires a ScreenViewport...
        }
    }
    return new RasterAttachmentViewport(_view, _rect, _attachment);
}
/** Draws a 3d view with camera enabled into a sheet view by producing an image of the view's contents offscreen. */
class RasterAttachment {
    constructor(view, props, sheetView) {
        // Render to a 2048x2048 view rect. Scale in Y to preserve aspect ratio.
        const maxSize = 2048;
        const rect = new ViewRect(0, 0, maxSize, maxSize);
        const height = maxSize * view.getAspectRatio() * view.getAspectRatioSkew();
        const skew = maxSize / height;
        view.setAspectRatioSkew(skew);
        if (true !== props.jsonProperties?.displayOptions?.preserveBackground) {
            // Make background color 100% transparent so that Viewport.readImageBuffer() will discard transparent pixels.
            const bgColor = sheetView.displayStyle.backgroundColor.withAlpha(0);
            view.displayStyle.backgroundColor = bgColor;
        }
        this._viewport = createRasterAttachmentViewport(view, rect, this);
        this._props = props;
        this._placement = Placement2d.fromJSON(props.placement);
        this._transform = this._placement.transform;
        this.zDepth = Frustum2d.depthFromDisplayPriority(props.jsonProperties?.displayPriority ?? 0);
    }
    dispose() {
        this._viewport?.dispose();
    }
    get viewAttachmentProps() {
        return this._props;
    }
    get viewport() {
        return this._viewport;
    }
    get areAllTileTreesLoaded() {
        return this._viewport?.areAllTileTreesLoaded ?? true;
    }
    addToScene(context) {
        // ###TODO: check viewport.wantViewAttachmentClipShapes
        if (!context.viewport.view.viewsCategory(this._props.category))
            return;
        if (context.viewport.wantViewAttachmentBoundaries) {
            const builder = context.createSceneGraphicBuilder(this._transform);
            builder.setSymbology(ColorDef.red, ColorDef.red, 2);
            builder.addRangeBox(Range3d.createRange2d(this._placement.bbox));
            context.outputGraphic(builder.finish());
        }
        if (!context.viewport.wantViewAttachments)
            return;
        if (this._graphics) {
            context.outputGraphic(this._graphics);
            return;
        }
        if (undefined === this._viewport)
            return;
        this._viewport.debugBoundingBoxes = context.viewport.debugBoundingBoxes;
        this._viewport.setTileSizeModifier(context.viewport.tileSizeModifier);
        this._viewport.renderFrame();
        if (this._graphics)
            context.outputGraphic(this._graphics);
    }
    discloseTileTrees(trees) {
        if (this._viewport)
            trees.disclose(this._viewport);
    }
    produceGraphics(context) {
        assert(context.viewport === this._viewport);
        this._graphics = this.createGraphics(this._viewport);
        this._viewport = dispose(this._viewport);
        if (undefined !== this._graphics)
            context.outputGraphic(this._graphics);
    }
    createGraphics(vp) {
        // Create a texture from the contents of the view.
        const image = vp.readImageBuffer({ upsideDown: true });
        if (undefined === image)
            return undefined;
        const debugImage = false; // set to true to open a window displaying the captured image.
        if (debugImage) {
            const url = imageBufferToPngDataUrl(image, false);
            if (url)
                openImageDataUrlInNewWindow(url, "Attachment");
        }
        const texture = IModelApp.renderSystem.createTexture({
            image: { source: image, transparency: TextureTransparency.Opaque },
        });
        if (!texture)
            return undefined;
        // Create a material for the texture
        const graphicParams = new GraphicParams();
        graphicParams.material = IModelApp.renderSystem.createRenderMaterial({ textureMapping: { texture } });
        // Apply the texture to a rectangular polyface.
        const depth = this.zDepth;
        const east = this._placement.bbox.low.x;
        const west = this._placement.bbox.high.x;
        const north = this._placement.bbox.low.y;
        const south = this._placement.bbox.high.y;
        const corners = [
            Point3d.create(east, north, depth),
            Point3d.create(west, north, depth),
            Point3d.create(west, south, depth),
            Point3d.create(east, south, depth),
        ];
        const params = [
            Point2d.create(0, 0),
            Point2d.create(1, 0),
            Point2d.create(1, 1),
            Point2d.create(0, 1),
        ];
        const strokeOptions = new StrokeOptions();
        strokeOptions.needParams = strokeOptions.shouldTriangulate = true;
        const polyfaceBuilder = PolyfaceBuilder.create(strokeOptions);
        polyfaceBuilder.addQuadFacet(corners, params);
        const polyface = polyfaceBuilder.claimPolyface();
        const graphicBuilder = IModelApp.renderSystem.createGraphicBuilder(Transform.createIdentity(), GraphicType.Scene, vp, this._props.id);
        graphicBuilder.activateGraphicParams(graphicParams);
        graphicBuilder.addPolyface(polyface, false);
        const graphic = graphicBuilder.finish();
        // Wrap the polyface in a GraphicBranch.
        const branch = new GraphicBranch(true);
        const vfOvrs = createDefaultViewFlagOverrides({ clipVolume: true, shadows: false, lighting: false, thematic: false });
        // Disable transparency - background pixels are 100% transparent so they will be discarded anyway. Other pixels are 100% opaque.
        vfOvrs.transparency = false;
        branch.setViewFlagOverrides(vfOvrs);
        branch.symbologyOverrides = new FeatureSymbology.Overrides();
        branch.entries.push(graphic);
        // Apply the attachment's clip, if any.
        let clipVolume;
        if (this._props.jsonProperties?.clip) {
            const clipVector = ClipVector.fromJSON(this._props.jsonProperties?.clip);
            if (clipVector.isValid)
                clipVolume = IModelApp.renderSystem.createClipVolume(clipVector);
        }
        return IModelApp.renderSystem.createGraphicBranch(branch, this._transform, { clipVolume });
    }
    collectStatistics(stats) {
        if (this._graphics)
            this._graphics.collectStatistics(stats);
    }
}
//# sourceMappingURL=SheetViewState.js.map