import { CanvasManagerBase } from "./CanvasManagerBase";
import { IModelSizeListener } from "../Model/ModelManipulator";
import { ILayoutPointResolver } from "./CanvasItemsManager";
import { IViewChangesListener, IZoomChangesListener, AutoZoomMode } from "../Settings";
import { EventDispatcher } from "../Utils";
import { Rectangle } from "@devexpress/utils/lib/geometry/rectangle";
import { Offsets } from "@devexpress/utils/lib/geometry/offsets";
import { Size } from "@devexpress/utils/lib/geometry/size";
import { Point } from "@devexpress/utils/lib/geometry/point";
import { IScrollView, IScrollViewListener } from "./ScrollView";
import { GroupPrimitive } from "./Primitives/GroupPrimitive";
import { ClipPathPrimitive } from "./Primitives/ClipPathPrimitive";
import { RectanglePrimitive } from "./Primitives/RectaglePrimitive";
import { RenderUtils } from "./Utils";
import { IMouseOperationsListener } from "../Events/Event";
import { ShadowFilterPrimitive } from "./Primitives/ShadowFilterPrimitive";
import { EmptyStyle } from "../Model/Style";
import { DOMManipulator } from "./DOMManipulator";
import { OrientationInfo, ICanvasViewManager } from "./ICanvasViewManager";
import { DomUtils } from "@devexpress/utils/lib/utils/dom";
import { UnitConverter } from "@devexpress/utils/lib/class/unit-converter";

export const CANVAS_MIN_PADDING = 8;
export const CANVAS_SCROLL_PADDING = 18;
export const CROP_OFFSET = 40;

const DRAG_SCROLL_CSSCLASS = "dxdi-drag-scroll";
const DRAG_ITEM_CSSCLASS = "dxdi-drag-item";

declare type Transition = { translate: Point, scroll: Point };

export class CanvasViewManager extends CanvasManagerBase implements IModelSizeListener, ICanvasViewManager, ILayoutPointResolver, IViewChangesListener, IZoomChangesListener, IMouseOperationsListener, IScrollViewListener {
    private modelSize: Size;
    private containerSize: Size;
    private paddings: Offsets = new Offsets(0, 0, 0, 0);
    private scroll: Point = new Point(0, 0);
    private simpleView: boolean;
    private crop: Offsets = Offsets.empty();
    private lockAutoZoom: boolean = false;
    autoZoom: AutoZoomMode;
    autoZoomLocked: boolean;
    private autoScrollLocker: number = 0;
    protected scrollView: IScrollView;
    private fixedZoomLevel: number;

    readonly pageClipPathId = RenderUtils.generateSvgElementId("page-clip");
    readonly pageShadowId = RenderUtils.generateSvgElementId("page-shadow");

    canvasElement: SVGGElement;
    pageElement: SVGGElement;
    onViewChanged: EventDispatcher<ICanvasViewListener> = new EventDispatcher();

    constructor(scrollView: IScrollView, protected svgElement: SVGSVGElement, modelSize: Size, fixedZoomLevel: number, autoZoom: AutoZoomMode, simpleView: boolean, rectangle: Rectangle, dom: DOMManipulator, instanceId: string) {
        super(fixedZoomLevel, dom, instanceId);
        scrollView.onScroll.add(this);
        modelSize = modelSize.clone().applyConverter(UnitConverter.twipsToPixelsF);
        this.scrollView = scrollView;
        this.modelSize = modelSize;
        this.simpleView = simpleView;
        this.fixedZoomLevel = fixedZoomLevel;
        this.autoZoom = autoZoom;
        this.crop = this.rectangleToCrop(rectangle, modelSize);
        this.updateElements(modelSize.clone().multiply(fixedZoomLevel, fixedZoomLevel), Point.zero(), simpleView); 
        this.getOrCreateElement("shadow", new ShadowFilterPrimitive(this.pageShadowId), this.svgElement);
        this.containerSize = scrollView.getSize();
    }

    adjust(resetPaddings?: OrientationInfo, saveVerticalScroll?: boolean) {
        let offset: Offsets;
        if(!resetPaddings) {
            resetPaddings = { vertical: false, horizontal: false };
            offset = Offsets.empty();
        }
        this.containerSize = this.scrollView.getSize();
        this.adjustCore(this.modelSize, this.fixedZoomLevel, this.autoZoom, this.simpleView, this.crop, resetPaddings, this.containerSize, offset, saveVerticalScroll);
        this.tryNormalizePaddings();
    }

    notifyModelSizeChanged(size: Size, offset?: Offsets) {
        size = size.clone().applyConverter(UnitConverter.twipsToPixelsF);
        const resetPaddings = { horizontal: !offset, vertical: !offset };
        this.adjustCore(size, this.fixedZoomLevel, this.autoZoom, this.simpleView, this.crop, resetPaddings, this.containerSize, offset && offset.clone().applyConverter(UnitConverter.twipsToPixelsF));
        this.modelSize = size;
    }
    notifyModelRectangleChanged(rectangle: Rectangle) {
        const crop = this.rectangleToCrop(rectangle, this.modelSize);
        if(!this.crop || !this.crop.equals(crop)) {
            if(this.simpleView)
                this.adjustCore(this.modelSize, this.fixedZoomLevel, this.autoZoom, this.simpleView, crop, { horizontal: false, vertical: false }, this.containerSize, Offsets.empty());
            this.crop = crop;
        }
    }
    notifySnapPointPositionChanged(point: Point) { }

    notifyZoomChanged(fixedZoomLevel: number, autoZoom: AutoZoomMode) {
        this.adjustCore(this.modelSize, fixedZoomLevel, autoZoom, this.simpleView, this.crop, {
            horizontal: false,
            vertical: false
        }, this.containerSize);
        this.fixedZoomLevel = fixedZoomLevel;
        this.autoZoom = autoZoom;
    }

    notifyViewChanged(simpleView: boolean) {
        this.adjustCore(this.modelSize, this.fixedZoomLevel, this.autoZoom, simpleView, this.crop, { vertical: true, horizontal: true }, this.containerSize);
        this.simpleView = simpleView;
    }
    notifyGridChanged(showGrid: boolean, gridSize: number) { }

    notifyDragStart(itemKeys: string[]) {
        this.lockAutoZoom = true;
        DomUtils.addClassName(this.svgElement, DRAG_ITEM_CSSCLASS);
    }
    notifyDragEnd(itemKeys: string[]) {
        this.lockAutoZoom = false;
        DomUtils.removeClassName(this.svgElement, DRAG_ITEM_CSSCLASS);

        this.adjustAfterDragEnd();
    }
    adjustAfterDragEnd() {
        if(this.autoZoom && !this.autoZoomLocked)
            this.adjust({ horizontal: true, vertical: this.autoZoom === AutoZoomMode.FitContent }, this.autoZoom === AutoZoomMode.FitToWidth);
    }
    notifyShowContextToolbox() {
        this.autoZoomLocked = true;
    }
    notifyHideContextToolbox() {
        this.autoZoomLocked = false;
        this.adjustAfterDragEnd();
    }

    notifyDragScrollStart() {
        DomUtils.addClassName(this.svgElement, DRAG_SCROLL_CSSCLASS);
    }
    notifyDragScrollEnd() {
        DomUtils.removeClassName(this.svgElement, DRAG_SCROLL_CSSCLASS);
    }
    notifyScrollChanged(getScroll: () => Point) {
        this.scroll = getScroll();
    }

    checkFitToCanvas(containerSize?: Size): OrientationInfo {
        containerSize = containerSize || this.containerSize;
        const scrollSize = this.scrollView.getScrollBarWidth();
        containerSize = containerSize.clone().offset(-CANVAS_MIN_PADDING * 2, -CANVAS_MIN_PADDING * 2).nonNegativeSize();
        const modelAbsSize = this.getActualModelSizeWithoutZoom(this.modelSize, this.simpleView, this.crop).clone().multiply(this.actualZoom, this.actualZoom);
        const scrollbars = this.checkScrollBars(containerSize, scrollSize, modelAbsSize, Offsets.empty());
        containerSize = containerSize.clone().offset(scrollbars.vertical ? -scrollSize : 0, scrollbars.horizontal ? -scrollSize : 0).nonNegativeSize();
        return {
            vertical: containerSize.height >= modelAbsSize.height,
            horizontal: containerSize.width >= modelAbsSize.width
        };
    }

    private rectangleToCrop(rectangle: Rectangle, modelSize: Size): Offsets {
        const absRectangle = rectangle.clone().applyConverter(UnitConverter.twipsToPixelsF);
        return new Offsets(
            this.correctCrop(absRectangle.x),
            this.correctCrop(modelSize.width - absRectangle.right),
            this.correctCrop(absRectangle.y),
            this.correctCrop(modelSize.height - absRectangle.bottom)
        );
    }
    private correctCrop(newVal: number): number {
        return CROP_OFFSET * Math.floor(newVal / CROP_OFFSET);
    }
    private setActualZoom(actualZoom: number) {
        if(this.actualZoom !== actualZoom) {
            this.actualZoom = actualZoom;
            this.raiseActualZoomChanged();
        }
    }
    getActualAutoZoomLevel(autoZoom: AutoZoomMode): number {
        if(autoZoom === AutoZoomMode.Disabled)
            return this.actualZoom;
        const containerSize = this.containerSize;
        const scrollbarWidth = this.scrollView.getScrollBarWidth();
        const actualModelSizeWithoutZoom = this.getActualModelSizeWithoutZoom(this.modelSize, this.simpleView, this.crop);
        return this.getActualAutoZoom(containerSize, scrollbarWidth, actualModelSizeWithoutZoom, autoZoom);
    }
    private getActualZoom(containerSize: Size, scrollbarWidth: number, actualModelSizeWithoutZoom: Size, fixedZoom: number, autoZoom: AutoZoomMode) {
        return this.lockAutoZoom ? this.actualZoom :
            autoZoom === AutoZoomMode.Disabled ? fixedZoom : this.getActualAutoZoom(containerSize, scrollbarWidth, actualModelSizeWithoutZoom, autoZoom);
    }
    private getActualAutoZoom(containerSize: Size, scrollbarWidth: number, actualModelSizeWithoutZoom: Size, autoZoom: AutoZoomMode): number {
        if(containerSize.width === 0 || containerSize.height === 0)
            return 1;
        if(autoZoom === AutoZoomMode.FitContent)
            return Math.min(
                (containerSize.width - CANVAS_MIN_PADDING * 2) / actualModelSizeWithoutZoom.width,
                (containerSize.height - CANVAS_MIN_PADDING * 2) / actualModelSizeWithoutZoom.height,
                1);

        return Math.min((containerSize.width - CANVAS_MIN_PADDING * 2 - scrollbarWidth) / actualModelSizeWithoutZoom.width, 1);
    }
    raiseActualZoomChanged() : void {
        this.onViewChanged.raise1(l => l.notifyActualZoomChanged(this.actualZoom));
    }
    tryNormalizePaddings(): void {
        const scrollbarWidth = this.scrollView.getScrollBarWidth();
        const actualModelSize = this.getActualModelSizeWithoutZoom(this.modelSize, this.simpleView, this.crop).clone().multiply(this.actualZoom, this.actualZoom);

        const translate = new Point(this.paddings.left, this.paddings.top);
        const currentTail = new Size(this.paddings.right, this.paddings.bottom);
        const tail = this.getTailSpace(translate, this.scroll, actualModelSize, this.containerSize, scrollbarWidth);
        if(!tail.equals(currentTail))
            this.applyChanges(new Offsets(translate.x, tail.width, translate.y, tail.height), actualModelSize, this.simpleView, this.crop.clone().multiply(this.actualZoom));
    }
    scrollBy(offset: Point): Point {
        let scroll = this.scroll;
        const containerSize = this.containerSize;
        const scrollbarWidth = this.scrollView.getScrollBarWidth();
        const actualModelSize = this.getActualModelSizeWithoutZoom(this.modelSize, this.simpleView, this.crop).clone().multiply(this.actualZoom, this.actualZoom);
        const scrollbars = this.checkScrollBars(containerSize, scrollbarWidth, actualModelSize, this.paddings);

        let translate = new Point(this.paddings.left, this.paddings.top);
        let tail = new Size(this.paddings.right, this.paddings.bottom);

        ({ scroll, offset } = this.changeScrollByOffset(translate, scroll, tail, actualModelSize, offset, containerSize, scrollbars));
        ({ translate, offset } = this.changeTranslateByOffset(translate, tail, offset, scrollbars));
        ({ translate, scroll } = this.cropHiddenHead(translate, scroll));

        tail = this.getTailSpace(translate, scroll, actualModelSize, containerSize, scrollbarWidth);
        this.applyChanges(new Offsets(translate.x, tail.width, translate.y, tail.height), actualModelSize, this.simpleView, this.crop.clone().multiply(this.actualZoom), scroll);
        return offset;
    }

    private changeScrollByOffset(curTranslate: Point, curScroll: Point, curTail: Size, modelSize: Size, curOffset: Point, containerSize: Size, scrollbars: OrientationInfo): { scroll: Point, offset: Point } {
        const scroll = curScroll.clone();
        const offset = curOffset.clone();
        if(curOffset.x && scrollbars.horizontal)
            scroll.x -= (offset.x = -this.getScrollDeltaByOffset(curOffset.x, curScroll.x, curTranslate.x + modelSize.width + curTail.width, containerSize.width, scrollbars.vertical));
        if(curOffset.y && scrollbars.vertical)
            scroll.y -= (offset.y = -this.getScrollDeltaByOffset(curOffset.y, curScroll.y, curTranslate.y + modelSize.height + curTail.height, containerSize.height, scrollbars.horizontal));
        return { scroll, offset };
    }

    private changeTranslateByOffset(curTranslate: Point, curTail: Size, curOffset: Point, scrollbars: OrientationInfo): { translate: Point, offset: Point } {
        const translate = curTranslate.clone();
        const offset = curOffset.clone();
        if(curOffset.x && !scrollbars.horizontal)
            translate.x += (offset.x = this.getTranslateDeltaByOffset(curOffset.x, translate.x, curTail.width));
        if(curOffset.y && !scrollbars.vertical)
            translate.y += (offset.y = this.getTranslateDeltaByOffset(curOffset.y, translate.y, curTail.height));
        return { translate, offset };
    }

    private getScrollDeltaByOffset(offset: number, scroll: number, commonWidth: number, containerWidth: number, hasScrollbar: boolean) {
        if(offset > 0)
            return -Math.min(scroll, offset);
        const maxScroll = commonWidth - (containerWidth - (hasScrollbar ? this.scrollView.getScrollBarWidth() : 0));
        return Math.min(maxScroll - scroll, -offset);
    }
    private getTranslateDeltaByOffset(offset: number, headPadding: number, tailPadding: number) {
        if(!offset) return 0;
        return offset < 0 ?
            -Math.min(headPadding - CANVAS_MIN_PADDING, -offset) :
            Math.min(tailPadding - CANVAS_MIN_PADDING, offset);
    }

    private getActualModelSizeWithoutZoom(originModelSize: Size, simpleView: boolean, crop: Offsets): Size {
        return simpleView && crop ? originModelSize.clone().offset(-crop.horizontal, -crop.vertical).nonNegativeSize() : originModelSize;
    }
    setScrollTo(modelPoint: Point, offsetPoint?: Point) {
        const containerSize = this.containerSize;

        const shift = this.getVisibileAreaAbsShift();
        const absPoint = modelPoint
            .clone().applyConverter(UnitConverter.twipsToPixelsF)
            .clone().multiply(this.actualZoom, this.actualZoom)
            .clone().offset(shift.x, shift.y);
        const scroll = this.scroll;

        if(!offsetPoint) {
            if(absPoint.x < 0)
                scroll.x += absPoint.x - CANVAS_MIN_PADDING;
            if(absPoint.y < 0)
                scroll.y += absPoint.y - CANVAS_MIN_PADDING;
            if(absPoint.x > containerSize.width)
                scroll.x += (absPoint.x - containerSize.width + CANVAS_MIN_PADDING);
            if(absPoint.y > containerSize.height)
                scroll.y += (absPoint.y - containerSize.height + CANVAS_MIN_PADDING);
        }
        else {
            scroll.x += absPoint.x - offsetPoint.x;
            scroll.y += absPoint.y - offsetPoint.y;
        }
        this.setScroll(scroll);
    }
    scrollIntoView(rectangle: Rectangle) {
        const shift = this.getVisibileAreaAbsShift(true);
        rectangle = rectangle
            .clone()
            .applyConverter(UnitConverter.twipsToPixelsF)
            .multiply(this.actualZoom, this.actualZoom)
            .moveRectangle(shift.x, shift.y);
        const scroll = this.scroll;
        const container = this.containerSize;
        if(rectangle.x >= scroll.x && rectangle.y >= scroll.y && rectangle.right <= scroll.x + container.width && rectangle.bottom <= scroll.y + container.height)
            return;
        const newScroll = scroll.clone();
        if(rectangle.x < scroll.x)
            newScroll.x = rectangle.x - CANVAS_SCROLL_PADDING;
        else if(rectangle.right > scroll.x + container.width) 
            newScroll.x = Math.min(rectangle.x - CANVAS_SCROLL_PADDING, rectangle.right + CANVAS_SCROLL_PADDING - container.width);
        if(rectangle.y < scroll.y)
            newScroll.y = rectangle.y - CANVAS_SCROLL_PADDING;
        else
            newScroll.y = Math.min(rectangle.y - CANVAS_SCROLL_PADDING, rectangle.bottom + CANVAS_SCROLL_PADDING - container.height);
        this.setScroll(newScroll);
    }

    private setScroll(pt: Point) {
        const modelAbsSize = this.modelSize.clone().multiply(this.actualZoom, this.actualZoom);
        pt.x = Math.max(0, Math.min(pt.x, modelAbsSize.width + this.paddings.horizontal - this.containerSize.width));
        pt.y = Math.max(0, Math.min(pt.y, modelAbsSize.height + this.paddings.vertical - this.containerSize.height));
        this.dom.changeByFunc(null, () => {
            this.scrollView.setScroll(pt.x, pt.y);
        });
        this.scroll = pt.clone();
    }

    private updateElements(modelAbsSize: Size, translate: Point, simpleView: boolean) {
        this.updatePageElement(modelAbsSize, translate, simpleView);
        this.updateCanvasElement(translate);
    }
    private updateCanvasElement(translate: Point) {
        this.canvasElement = this.getOrCreateElement("dxdi-main", new GroupPrimitive([], "dxdi-main", null, null, el => {
            el.setAttribute("transform", `translate(${Math.round(translate.x)}, ${Math.round(translate.y)})`);
        }), this.svgElement);
    }
    private updatePageElement(modelAbsSize: Size, translate: Point, simpleView: boolean) {
        if(simpleView)
            this.updatePageElementCore("", 0, 0, modelAbsSize.width, modelAbsSize.height);
        else {
            const x = translate.x;
            const y = translate.y;
            const modelAbsWidth = modelAbsSize.width;
            const modelAbsHeight = modelAbsSize.height;
            this.createPageShadow(x, y, modelAbsWidth, modelAbsHeight);
            this.updatePageElementCore(this.pageClipPathId, Math.round(x), Math.round(y), modelAbsWidth, modelAbsHeight);
        }
    }
    private createPageShadow(left: number, top: number, width: number, height: number) : void {
        this.getOrCreateElement("pageShadowRect", new RectanglePrimitive(
            left.toString(), top.toString(), width.toString(), height.toString(),
            new EmptyStyle({ "filter": RenderUtils.getUrlPathById(this.pageShadowId) }),
            "dxdi-page-shadow"), this.svgElement, <SVGElement> this.svgElement.firstChild);
    }
    private updatePageElementCore(groupClipPathId: string, translateX: number, translateY: number, modelAbsWidth: number, modelAbsHeight: number): void {
        this.pageElement = this.getOrCreateElement("page", new GroupPrimitive([], "dxdi-page", null, groupClipPathId, el => {
            el.setAttribute("transform", `translate(${translateX}, ${translateY})`);
        }), this.svgElement);
        this.getOrCreateElement("pageClip", this.createPageClipPathPrimitive(modelAbsWidth, modelAbsHeight), this.svgElement);
    }
    private createPageClipPathPrimitive(modelAbsWidth: number, modelAbsHeight: number): ClipPathPrimitive {
        return new ClipPathPrimitive(this.pageClipPathId, [new RectanglePrimitive(0, 0, modelAbsWidth.toString(), modelAbsHeight.toString())]);
    }

    adjustCore(newModelSize: Size, fixedZoomLevel: number, autoZoom: AutoZoomMode, simpleView: boolean, crop: Offsets, resetPaddings: OrientationInfo, containerSize: Size, offset?: Offsets,
        saveVerticalScroll?: boolean): void {
        const actualModelSizeWithoutZoom = this.getActualModelSizeWithoutZoom(newModelSize, simpleView, crop);
        if(!this.lockAutoZoom && (autoZoom || !offset || !this.modelSize)) {
            const scrollbarWidth = this.scrollView.getScrollBarWidth();
            const actualZoom = this.getActualZoom(containerSize, scrollbarWidth, actualModelSizeWithoutZoom, fixedZoomLevel, autoZoom);
            if(autoZoom && actualZoom === this.actualZoom && (!resetPaddings.horizontal || (!resetPaddings.vertical && !saveVerticalScroll)))
                this.resizeView(actualModelSizeWithoutZoom, actualZoom, containerSize, simpleView, crop, offset || Offsets.empty());
            else {
                this.resetView(actualModelSizeWithoutZoom, actualZoom, containerSize, simpleView, crop, resetPaddings);
                this.setActualZoom(actualZoom);
            }
        }
        else
            this.resizeView(actualModelSizeWithoutZoom, this.actualZoom, containerSize, simpleView, crop, offset || Offsets.empty());
    }

    private resetView(actualModelSizeWithoutZoom: Size, actualZoom: number, containerSize: Size, simpleView: boolean, cropWithoutZoom: Offsets, toReset: OrientationInfo) {
        const actualModelSize = actualModelSizeWithoutZoom.clone().multiply(actualZoom, actualZoom);
        const paddings = Offsets.fromNumber(CANVAS_MIN_PADDING);
        toReset = toReset || { horizontal: true, vertical: true };
        if(!toReset.horizontal && this.paddings) {
            paddings.left = this.paddings.left;
            paddings.right = this.paddings.right;
        }
        if(!toReset.vertical && this.paddings) {
            paddings.top = this.paddings.top;
            paddings.bottom = this.paddings.bottom;
        }
        const scrollbars = this.checkScrollBars(containerSize, this.scrollView.getScrollBarWidth(), actualModelSize, paddings);
        const scrollBarWidth = this.scrollView.getScrollBarWidth();
        const scroll = (toReset.horizontal || toReset.vertical) ? this.scroll : undefined;
        if(toReset.horizontal) {
            const paddingsH = Math.max((containerSize.width - (scrollbars.vertical ? scrollBarWidth : 0) - actualModelSize.width) / 2, CANVAS_MIN_PADDING);
            paddings.left = paddingsH;
            paddings.right = paddingsH;
            scroll.x = 0;
        }
        if(toReset.vertical) {
            const paddingsV = Math.max((containerSize.height - (scrollbars.horizontal ? scrollBarWidth : 0) - actualModelSize.height) / 2, CANVAS_MIN_PADDING);
            paddings.top = paddingsV;
            paddings.bottom = paddingsV;
            scroll.y = 0;
        }
        this.applyChanges(paddings, actualModelSize, simpleView, cropWithoutZoom.clone().multiply(actualZoom), scroll);
    }
    private resizeView(actualModelSizeWithoutZoom: Size, actualZoom: number, containerSize: Size, simpleView: boolean, cropWithoutZoom: Offsets, offset: Offsets) {
        const oldZoom = this.actualZoom;
        const oldCrop = this.simpleView && this.crop ? this.crop.clone().multiply(oldZoom) : Offsets.empty();
        const actualModelSize = actualModelSizeWithoutZoom.clone().multiply(actualZoom, actualZoom);
        offset = offset.clone().multiply(actualZoom);
        const newCrop = simpleView && cropWithoutZoom ? cropWithoutZoom.clone().multiply(actualZoom) : Offsets.empty();
        let translate: Point = new Point(this.paddings.left, this.paddings.top);
        let scroll = this.scroll;

        ({ translate, scroll } = this.applyOffset(translate, scroll, oldCrop, newCrop, offset));
        ({ translate, scroll } = this.cropHiddenHead(translate, scroll));
        const tailSpace = this.getTailSpace(translate, scroll, actualModelSize, containerSize, this.scrollView.getScrollBarWidth());

        if(!simpleView) {
            const maxTailSpaceWidth = containerSize.width - CANVAS_SCROLL_PADDING;
            const maxTailSpaceHeight = containerSize.height - CANVAS_SCROLL_PADDING;
            if(offset.left < 0)
                if(translate.x > maxTailSpaceWidth) {
                    translate.x = maxTailSpaceWidth;
                    scroll.x = 0;
                }
            if(offset.right < 0)
                if(tailSpace.width > maxTailSpaceWidth) {
                    tailSpace.width = maxTailSpaceWidth;
                    if(scroll.x > actualModelSize.width)
                        scroll.x = actualModelSize.width;
                }
            if(offset.top < 0)
                if(translate.y > maxTailSpaceHeight) {
                    translate.y = maxTailSpaceHeight;
                    scroll.y = 0;
                }
            if(offset.bottom < 0)
                if(tailSpace.height > maxTailSpaceHeight) {
                    tailSpace.height = maxTailSpaceHeight;
                    if(scroll.y > actualModelSize.height)
                        scroll.y = actualModelSize.height;
                }
        }

        const newPaddings = new Offsets(translate.x, tailSpace.width, translate.y, tailSpace.height);
        this.applyChanges(newPaddings, actualModelSize, simpleView, newCrop, scroll);
    }

    applyChanges(paddings: Offsets, actualModelSize: Size, simpleView: boolean, crop: Offsets, scroll?: Point) {
        let translate = new Point(paddings.left, paddings.top);
        if(simpleView && crop)
            translate = translate.clone().offset(-crop.left, -crop.top);
        this.updateElements(actualModelSize, translate, simpleView);
        this.setSvgSize(actualModelSize.width + paddings.horizontal, actualModelSize.height + paddings.vertical);
        this.onViewChanged.raise1(l => l.notifyViewAdjusted(new Point(translate.x, translate.y)));
        if(scroll) {
            this.lockAutoScroll();
            scroll && this.dom.changeByFunc(this.scrollView, s => {
                s.setScroll(scroll.x, scroll.y);
                this.unlockAutoScroll();
            });
            this.scroll = scroll;
        }
        this.paddings = paddings;
    }

    isAutoScrollLocked() {
        return this.autoScrollLocker !== 0;
    }
    private lockAutoScroll() {
        this.autoScrollLocker++;
    }
    private unlockAutoScroll() {
        this.autoScrollLocker--;
    }

    private applyOffset(curTranslate: Point, curScroll: Point, oldCrop: Offsets, newCrop: Offsets, modelOffset: Offsets): Transition {
        const translate = curTranslate.clone();
        const scroll = curScroll.clone();
        const offset = this.getActualOffset(oldCrop, newCrop, modelOffset);
        if(offset.left) {
            translate.x = Math.max(CANVAS_MIN_PADDING, translate.x - offset.left);
            scroll.x += offset.left - (curTranslate.x - translate.x);
        }
        if(offset.top) {
            translate.y = Math.max(CANVAS_MIN_PADDING, translate.y - offset.top);
            scroll.y += offset.top - (curTranslate.y - translate.y);
        }
        return { translate, scroll };
    }
    private cropHiddenHead(curTranslate: Point, curScroll: Point): Transition {
        const scroll = curScroll.clone();
        const translate = curTranslate.clone();
        if(scroll.x && translate.x > CANVAS_MIN_PADDING) {
            const delta = translate.x - Math.max(CANVAS_MIN_PADDING, translate.x - scroll.x);
            translate.x -= delta;
            scroll.x -= delta;
        }
        if(scroll.y && translate.y > CANVAS_MIN_PADDING) {
            const delta = translate.y - Math.max(CANVAS_MIN_PADDING, translate.y - scroll.y);
            translate.y -= delta;
            scroll.y -= delta;
        }
        return { translate, scroll };
    }
    private getTailSpace(curTranslate: Point, curScroll: Point, newModelAbsSize: Size, containerSize: Size, scrollbarWidth: number): Size {
        const translate = curTranslate.clone();
        const scroll = curScroll.clone();

        let right = Math.max(containerSize.width + scroll.x - (translate.x + newModelAbsSize.width), CANVAS_MIN_PADDING);
        let bottom = Math.max(containerSize.height + scroll.y - (translate.y + newModelAbsSize.height), CANVAS_MIN_PADDING);
        const scrollbars = this.checkScrollBars(containerSize, scrollbarWidth, newModelAbsSize, new Offsets(translate.x, right, translate.y, bottom));
        if(scrollbars.vertical)
            right = Math.max(CANVAS_MIN_PADDING, right - scrollbarWidth);
        if(scrollbars.horizontal)
            bottom = Math.max(CANVAS_MIN_PADDING, bottom - scrollbarWidth);
        return new Size(right, bottom);
    }

    private getActualOffset(oldCrop: Offsets, newCrop: Offsets, docOffset: Offsets): Offsets {
        return new Offsets(
            -(newCrop.left - oldCrop.left) + docOffset.left,
            -(newCrop.right - oldCrop.right) + docOffset.right,
            -(newCrop.top - oldCrop.top) + docOffset.top,
            -(newCrop.bottom - oldCrop.bottom) + docOffset.bottom
        );
    }

    private checkScrollBars(containerSize: Size, scrollBarWidth: number, modelAbsSize: Size, paddings: Offsets): OrientationInfo {
        let hasHorizontalScroll = containerSize.width < modelAbsSize.width + paddings.horizontal;
        let hasVerticalScroll = containerSize.height < modelAbsSize.height + paddings.vertical;
        if(hasHorizontalScroll && !hasVerticalScroll)
            hasVerticalScroll = containerSize.height - scrollBarWidth < modelAbsSize.height + paddings.vertical;
        if(hasVerticalScroll && !hasHorizontalScroll)
            hasHorizontalScroll = containerSize.width - scrollBarWidth < modelAbsSize.width + paddings.horizontal;
        return { horizontal: hasHorizontalScroll, vertical: hasVerticalScroll };
    }
    private lastWidth: number;
    private lastHeight: number;
    private setSvgSize(width: number, height: number) {
        if(width !== this.lastWidth || height !== this.lastHeight) {
            this.dom.changeByFunc(this.svgElement, e => RenderUtils.updateSvgElementSize(e, width, height));
            this.lastWidth = width;
            this.lastHeight = height;
        }
    }
    private getVisibileAreaAbsShift(excludeScroll?: boolean): Point {
        const scroll = this.scroll;
        const paddings = this.paddings.clone();
        const simpleView = this.simpleView;
        const cropLeft = simpleView && this.crop ? this.crop.left * this.actualZoom : 0;
        const cropTop = simpleView && this.crop ? this.crop.top * this.actualZoom : 0;
        return new Point(
            paddings.left - cropLeft - (excludeScroll ? 0 : scroll.x),
            paddings.top - cropTop - (excludeScroll ? 0 : scroll.y)
        );
    }

    getModelPoint(absolutePoint: Point, checkScrollArea?: boolean): Point {
        const shift = this.getVisibileAreaAbsShift();
        const modelPoint = absolutePoint
            .clone().offset(-shift.x, -shift.y)
            .multiply(1 / this.actualZoom, 1 / this.actualZoom);
        if(checkScrollArea) {
            const scrollSize = this.containerSize;
            if(absolutePoint.x < 0 || absolutePoint.y < 0 || absolutePoint.x > scrollSize.width || absolutePoint.y > scrollSize.height)
                return null;
            if(modelPoint.x < 0 || modelPoint.y < 0)
                return null;
            if(modelPoint.x > this.modelSize.width || modelPoint.y > this.modelSize.height)
                return null;
        }
        return modelPoint.clone().applyConverter(UnitConverter.pixelsToTwips);
    }
    getAbsolutePoint(modelPoint: Point, excludeScroll?: boolean, checkScrollArea?: boolean): Point {
        const shift = this.getVisibileAreaAbsShift(excludeScroll);
        const absPoint = modelPoint
            .clone().multiply(this.actualZoom, this.actualZoom)
            .clone().applyConverter(UnitConverter.twipsToPixelsF)
            .clone().offset(shift.x, shift.y);
        if(checkScrollArea) {
            if(absPoint.x < 0 || absPoint.y < 0)
                return null;
            const scrollSize = this.containerSize;
            if(absPoint.x > scrollSize.width || absPoint.y > scrollSize.height)
                return null;
        }
        return absPoint;
    }
}

export interface ICanvasViewListener {
    notifyViewAdjusted(canvasOffset: Point);
    notifyActualZoomChanged(actualZoom: number);
}
