// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
using System.Drawing;
using System.Drawing.Printing;
using System.Runtime.InteropServices;
using Windows.Win32.UI.Accessibility;

namespace System.Windows.Forms;

/// <summary>
///  The raw "preview" part of print previewing, without any dialogs or buttons. Most <see cref="PrintPreviewControl"/>
///  objects are found on <see cref="PrintPreviewDialog"/> objects, but they don't have to be.
/// </summary>
[DefaultProperty(nameof(Document))]
[SRDescription(nameof(SR.DescriptionPrintPreviewControl))]
public partial class PrintPreviewControl : Control
{
    private const int ScrollSmallChange = 5;
    private const double DefaultZoom = .3;

    // Spacing per page, in mm
    private const int Border = 10;

    private static readonly object s_startPageChangedEvent = new();

    private PrintDocument? _document;
    private PreviewPageInfo[]? _pageInfo; // null if needs refreshing
    private int _startPage;  // 0-based
    private int _rows = 1;
    private int _columns = 1;
    private bool _autoZoom = true;
    private Size _virtualSize = new(1, 1);
    private Point _position = new(0, 0);

    private readonly int _focusHOffset = SystemInformation.HorizontalFocusThickness;
    private readonly int _focusVOffset = SystemInformation.VerticalFocusThickness;
    private readonly HorizontalScrollBar _hScrollBar;
    private readonly VerticalScrollBar _vScrollBar;
    private bool _scrollLayoutPending;

    // The following are all computed by ComputeLayout
    private bool _layoutOk;

    // 100ths of an inch, not pixels
    private Size _imageSize = Size.Empty;
    private Point _screenDPI = Point.Empty;
    private double _zoom = DefaultZoom;
    private bool _pageInfoCalcPending;
    private bool _exceptionPrinting;
    private bool _isForeColorSet;

    /// <summary>
    ///  Initializes a new instance of the <see cref="PrintPreviewControl"/> class.
    /// </summary>
    public PrintPreviewControl()
    {
        ResetBackColor();
        ResetForeColor();
        ForeColorChanged += (_, _) => _isForeColorSet = true;
        Size = new Size(100, 100);
        SetStyle(ControlStyles.ResizeRedraw, false);
        SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true);
        TabStop = false;

        _hScrollBar = new()
        {
            AccessibleName = SR.HScrollBarDefaultAccessibleName,
            Anchor = AnchorStyles.Left | AnchorStyles.Bottom | AnchorStyles.Right,
            Left = _focusHOffset,
            SmallChange = ScrollSmallChange,
            RightToLeft = RightToLeft.No,
            Visible = false,
            TabStop = false
        };

        _hScrollBar.ValueChanged += scrollBar_ValueChanged;
        Controls.Add(_hScrollBar);

        _vScrollBar = new()
        {
            AccessibleName = SR.VScrollBarDefaultAccessibleName,
            Anchor = AnchorStyles.Top | AnchorStyles.Right | AnchorStyles.Bottom,
            SmallChange = ScrollSmallChange,
            Top = _focusVOffset,
            Visible = false,
            TabStop = false
        };

        _vScrollBar.ValueChanged += scrollBar_ValueChanged;
        Controls.Add(_vScrollBar);
    }

    /// <summary>
    ///  Gets or sets a value indicating whether printing uses the anti-aliasing features of the operating system.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.PrintPreviewAntiAliasDescr))]
    public bool UseAntiAlias
    {
        get; set;
    }

    /// <summary>
    ///  Gets or sets a value indicating whether resizing the control or changing the number of pages shown
    ///  automatically adjusts the <see cref="Zoom"/> property.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(true)]
    [SRDescription(nameof(SR.PrintPreviewAutoZoomDescr))]
    public bool AutoZoom
    {
        get => _autoZoom;
        set
        {
            if (_autoZoom != value)
            {
                _autoZoom = value;
                InvalidateLayout();
            }
        }
    }

    /// <summary>
    ///  Gets or sets a value indicating how large the pages will appear.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.PrintPreviewZoomDescr))]
    [DefaultValue(DefaultZoom)]
    public double Zoom
    {
        get => _zoom;
        set
        {
            if (value <= 0)
            {
                throw new ArgumentException(SR.PrintPreviewControlZoomNegative);
            }

            _autoZoom = false;
            _zoom = value;
            InvalidateLayout();
        }
    }

    /// <summary>
    ///  Gets or sets a value indicating the document to preview.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(null)]
    [SRDescription(nameof(SR.PrintPreviewDocumentDescr))]
    public PrintDocument? Document
    {
        get => _document;
        set
        {
            _document = value;
            InvalidatePreview();
        }
    }

    /// <summary>
    ///  Gets or sets the number of pages displayed vertically down the screen.
    /// </summary>
    [DefaultValue(1)]
    [SRDescription(nameof(SR.PrintPreviewRowsDescr))]
    [SRCategory(nameof(SR.CatBehavior))]
    public int Rows
    {
        get => _rows;
        set
        {
            ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value);

            _rows = value;
            InvalidateLayout();
        }
    }

    /// <summary>
    ///  Gets or sets the number of pages displayed horizontally across the screen.
    /// </summary>
    [DefaultValue(1)]
    [SRCategory(nameof(SR.CatLayout))]
    [SRDescription(nameof(SR.PrintPreviewColumnsDescr))]
    public int Columns
    {
        get => _columns;
        set
        {
            ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value);

            _columns = value;
            InvalidateLayout();
        }
    }

    /// <summary>
    ///  Gets or sets the page number of the upper left page.
    /// </summary>
    [DefaultValue(0)]
    [SRDescription(nameof(SR.PrintPreviewStartPageDescr))]
    [SRCategory(nameof(SR.CatBehavior))]
    public int StartPage
    {
        get
        {
            int value = _startPage;
            if (_pageInfo is not null)
            {
                value = Math.Min(value, _pageInfo.Length - (_rows * _columns));
            }

            value = Math.Max(value, 0);

            return value;
        }
        set
        {
            ArgumentOutOfRangeException.ThrowIfNegative(value);

            int oldValue = StartPage;
            _startPage = value;
            if (oldValue != _startPage)
            {
                InvalidateLayout();
                OnStartPageChanged(EventArgs.Empty);
            }
        }
    }

    [SRCategory(nameof(SR.CatPropertyChanged))]
    [SRDescription(nameof(SR.RadioButtonOnStartPageChangedDescr))]
    public event EventHandler? StartPageChanged
    {
        add => Events.AddHandler(s_startPageChangedEvent, value);
        remove => Events.RemoveHandler(s_startPageChangedEvent, value);
    }

    protected virtual void OnStartPageChanged(EventArgs e)
    {
        if (Events[s_startPageChangedEvent] is EventHandler eh)
        {
            eh(this, e);
        }
    }

    /// <summary>
    ///  Gets or sets a value indicating whether control's elements are aligned to support locales using right-to-left fonts.
    /// </summary>
    [SRCategory(nameof(SR.CatAppearance))]
    [Localizable(true)]
    [AmbientValue(RightToLeft.Inherit)]
    [SRDescription(nameof(SR.ControlRightToLeftDescr))]
    public override RightToLeft RightToLeft
    {
        get => base.RightToLeft;
        set
        {
            base.RightToLeft = value;
            InvalidatePreview();
        }
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [Bindable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [AllowNull]
    public override string Text
    {
        get => base.Text;
        set => base.Text = value;
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new event EventHandler? TextChanged
    {
        add => base.TextChanged += value;
        remove => base.TextChanged -= value;
    }

    [DefaultValue(false)]
    [DispId(PInvokeCore.DISPID_TABSTOP)]
    public new bool TabStop
    {
        get => base.TabStop;
        set => base.TabStop = value;
    }

    [EditorBrowsable(EditorBrowsableState.Never)]
    public override void ResetBackColor() => BackColor = SystemColors.AppWorkspace;

    internal override bool ShouldSerializeBackColor() => !BackColor.Equals(SystemColors.AppWorkspace);

    [EditorBrowsable(EditorBrowsableState.Never)]
    public override void ResetForeColor()
    {
        ForeColor = SystemColors.ControlText;
        _isForeColorSet = false;
    }

    internal override bool ShouldSerializeForeColor() => _isForeColorSet;

    internal override bool SupportsUiaProviders => true;

    protected override AccessibleObject CreateAccessibilityInstance()
        => new PrintPreviewControlAccessibleObject(this);

    protected override void OnResize(EventArgs eventargs)
    {
        if (!IsHandleCreated)
        {
            return;
        }

        InvalidateLayout();
        base.OnResize(eventargs);
    }

    protected override void OnMouseDown(MouseEventArgs e)
    {
        if (!Focused)
        {
            Focus();
        }

        base.OnMouseDown(e);
    }

    protected override void OnGotFocus(EventArgs e)
    {
        Invalidate();
        base.OnGotFocus(e);
    }

    protected override void OnLostFocus(EventArgs e)
    {
        Invalidate();
        base.OnLostFocus(e);
    }

    protected override void OnPaintBackground(PaintEventArgs e) => PaintTransparentBackground(e, ClientRectangle);

    protected override void OnPaint(PaintEventArgs pevent)
    {
        bool isHighContrast = SystemInformation.HighContrast;
        Color backColor = GetBackColor(isHighContrast);
        using var backBrush = backColor.GetCachedSolidBrushScope();

        PaintResizeBox(pevent);
        PaintFocus(pevent, isHighContrast);

        if (_pageInfo is null || _pageInfo.Length == 0)
        {
            Rectangle rect = InsideRectangle;

            pevent.Graphics.FillRectangle(backBrush, rect);

            if (_pageInfo is not null || _exceptionPrinting)
            {
                DrawMessage(pevent.Graphics, rect, _exceptionPrinting);
            }
            else
            {
                BeginInvoke(new MethodInvoker(CalculatePageInfo));
            }
        }
        else
        {
            if (!_layoutOk)
            {
                ComputeLayout();
            }

            DrawPages(pevent.Graphics, InsideRectangle, _pageInfo, backBrush);
        }

        base.OnPaint(pevent); // raise paint event
    }

    /// <summary>
    ///  How big the control would be if the screen was infinitely large.
    /// </summary>
    [SRCategory(nameof(SR.CatLayout))]
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.ControlWithScrollbarsVirtualSizeDescr))]
    private Size VirtualSize
    {
        get => _virtualSize;
        set
        {
            SetVirtualSizeNoInvalidate(value);
            Invalidate();
        }
    }

    /// <summary>
    ///  The virtual coordinate of the upper left visible pixel.
    /// </summary>
    [SRCategory(nameof(SR.CatLayout))]
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.ControlWithScrollbarsPositionDescr))]
    private Point Position
    {
        get => _position;
        set
        {
            SetPositionNoInvalidate(value);
            Invalidate();
        }
    }

    /// <summary>
    ///  Equal to the vertical percentage of the entire control that is currently viewable.
    /// </summary>
    internal double VerticalViewSize => InsideRectangle.Height * 100.0 / VirtualSize.Height;

    /// <summary>
    ///  Equal to the horizontal percentage of the entire control that is currently viewable.
    /// </summary>
    internal double HorizontalViewSize => InsideRectangle.Width * 100.0 / VirtualSize.Width;

    private Rectangle InnerClientRectangle
    {
        get
        {
            Rectangle rect = ClientRectangle;
            rect.Inflate(-_focusHOffset, -_focusVOffset);
            rect.Width = Math.Max(0, rect.Width - 1);
            rect.Height = Math.Max(0, rect.Height - 1);

            return rect;
        }
    }

    private Rectangle InsideRectangle
    {
        get
        {
            Rectangle rect = InnerClientRectangle;

            if (_hScrollBar.Visible)
            {
                rect.Height -= _hScrollBar.Height;
            }

            if (_vScrollBar.Visible)
            {
                rect.Width -= _vScrollBar.Width;

                if (RightToLeft == RightToLeft.Yes)
                {
                    rect.X += _vScrollBar.Width;
                }
            }

            return rect;
        }
    }

    private Rectangle FocusRectangle => new(0, 0, Width - 1, Height - 1);

    private Rectangle ResizeBoxRectangle => new(_vScrollBar.Left, _hScrollBar.Top, _vScrollBar.Width, _hScrollBar.Height);

    // This function computes everything in terms of physical size (millimeters), not pixels
    private void ComputeLayout()
    {
        Debug.Assert(_pageInfo is not null, "Must call ComputePreview first");
        _layoutOk = true;
        if (_pageInfo.Length == 0)
        {
            return;
        }

        using GetDcScope hdc = new(HWND);
        _screenDPI = new Point(
            PInvokeCore.GetDeviceCaps(hdc, GET_DEVICE_CAPS_INDEX.LOGPIXELSX),
            PInvokeCore.GetDeviceCaps(hdc, GET_DEVICE_CAPS_INDEX.LOGPIXELSY));

        Size pageSize = _pageInfo[StartPage].PhysicalSize;
        Size controlPhysicalSize = PixelsToPhysical(Size, _screenDPI);

        if (_autoZoom)
        {
            double zoomX = ((double)controlPhysicalSize.Width - Border * (_columns + 1)) / (_columns * pageSize.Width);
            double zoomY = ((double)controlPhysicalSize.Height - Border * (_rows + 1)) / (_rows * pageSize.Height);
            _zoom = Math.Min(zoomX, zoomY);
        }

        _imageSize = new Size((int)(_zoom * pageSize.Width), (int)(_zoom * pageSize.Height));
        int virtualX = (_imageSize.Width * _columns) + Border * (_columns + 1);
        int virtualY = (_imageSize.Height * _rows) + Border * (_rows + 1);
        SetVirtualSizeNoInvalidate(PhysicalToPixels(new Size(virtualX, virtualY), _screenDPI));
    }

    // "Prints" the document to memory
    private void ComputePreview()
    {
        int oldStart = StartPage;

        if (_document is null)
        {
            _pageInfo = [];
        }
        else
        {
            PrintController oldController = _document.PrintController;
            PreviewPrintController previewController = new PreviewPrintController
            {
                UseAntiAlias = UseAntiAlias
            };

            _document.PrintController = new PrintControllerWithStatusDialog(
                previewController,
                SR.PrintControllerWithStatusDialog_DialogTitlePreview);

            _document.Print();
            _pageInfo = previewController.GetPreviewPageInfo();
            Debug.Assert(_pageInfo is not null, $"{nameof(PreviewPrintController)} did not give us preview info.");

            _document.PrintController = oldController;
        }

        if (oldStart != StartPage)
        {
            OnStartPageChanged(EventArgs.Empty);
        }
    }

    // Recomputes the sizes and positions of pages without forcing a new "preview print"
    private void InvalidateLayout()
    {
        if (!IsHandleCreated)
        {
            return;
        }

        _layoutOk = false;
        LayoutScrollBars();
        Invalidate();
    }

    /// <summary>
    ///  Refreshes the preview of the document.
    /// </summary>
    public void InvalidatePreview()
    {
        if (!IsHandleCreated)
        {
            return;
        }

        _pageInfo = null;
        _exceptionPrinting = false;
        SetVirtualSizeNoInvalidate(Size.Empty);
        InvalidateLayout();
    }

    private void CalculatePageInfo()
    {
        if (_pageInfoCalcPending)
        {
            return;
        }

        _pageInfoCalcPending = true;
        try
        {
            if (_pageInfo is null)
            {
                try
                {
                    ComputePreview();
                }
                catch
                {
                    _exceptionPrinting = true;
                    throw;
                }
                finally
                {
                    Invalidate();
                }
            }
        }
        finally
        {
            _pageInfoCalcPending = false;
        }
    }

    private void DrawMessage(Graphics g, Rectangle rect, bool isExceptionPrinting)
    {
        Color brushColor = _isForeColorSet ? ForeColor : SystemColors.ControlText;
        if (SystemInformation.HighContrast && Parent is Control parent)
        {
            brushColor = parent.BackColor;
        }

        using var brush = brushColor.GetCachedSolidBrushScope();

        using StringFormat format = new()
        {
            Alignment = StringAlignment.Center,
            LineAlignment = StringAlignment.Center
        };

        string message = isExceptionPrinting ? SR.PrintPreviewExceptionPrinting : SR.PrintPreviewNoPages;

        g.DrawString(message, Font, brush, rect, format);
    }

    private void DrawPages(Graphics g, Rectangle rect, PreviewPageInfo[] pages, Brush backBrush)
    {
        using GraphicsClipScope clipScope = new(g);
        g.SetClip(rect);

        _position.X = _hScrollBar.Value;
        _position.Y = _vScrollBar.Value;

        Size controlPhysicalSize = PixelsToPhysical(rect.Size, _screenDPI);

        // center pages on screen if small enough
        Point offset = new(
            Math.Max(0, (rect.Width - _virtualSize.Width) / 2),
            Math.Max(0, (rect.Height - _virtualSize.Height) / 2));
        offset.X -= Position.X;
        offset.Y -= Position.Y;

        int borderPixelsX = PhysicalToPixels(Border, _screenDPI.X);
        int borderPixelsY = PhysicalToPixels(Border, _screenDPI.Y);

        Rectangle[] pageRenderArea = new Rectangle[_rows * _columns];
        Point lastImageSize = Point.Empty;
        int maxImageHeight = 0;

        using (new GraphicsClipScope(g))
        {
            for (int row = 0; row < _rows; row++)
            {
                // Initialize our LastImageSize variable...
                lastImageSize.X = 0;
                lastImageSize.Y = maxImageHeight * row;

                for (int column = 0; column < _columns; column++)
                {
                    int imageIndex = StartPage + column + row * _columns;
                    if (imageIndex < pages.Length)
                    {
                        Size pageSize = pages[imageIndex].PhysicalSize;
                        if (_autoZoom)
                        {
                            double zoomX = ((double)controlPhysicalSize.Width - Border * (_columns + 1))
                                / (_columns * pageSize.Width);
                            double zoomY = ((double)controlPhysicalSize.Height - Border * (_rows + 1))
                                / (_rows * pageSize.Height);
                            _zoom = Math.Min(zoomX, zoomY);
                        }

                        _imageSize = new Size((int)(_zoom * pageSize.Width), (int)(_zoom * pageSize.Height));
                        Size imagePixels = PhysicalToPixels(_imageSize, _screenDPI);

                        int x = offset.X + borderPixelsX * (column + 1) + lastImageSize.X;
                        int y = offset.Y + borderPixelsY * (row + 1) + lastImageSize.Y;

                        lastImageSize.X += imagePixels.Width;

                        // The Height is the Max of any PageHeight..
                        maxImageHeight = Math.Max(maxImageHeight, imagePixels.Height);

                        pageRenderArea[imageIndex - StartPage] = new Rectangle(x, y, imagePixels.Width, imagePixels.Height);
                        g.ExcludeClip(pageRenderArea[imageIndex - StartPage]);
                    }
                }
            }

            g.FillRectangle(backBrush, rect);
        }

        for (int i = 0; i < pageRenderArea.Length; i++)
        {
            if (i + StartPage < pages.Length)
            {
                Rectangle box = pageRenderArea[i];
                g.DrawRectangle(Pens.Black, box);
                using (var brush = ForeColor.GetCachedSolidBrushScope())
                {
                    g.FillRectangle(brush, box);
                }

                box.Inflate(-1, -1);
                if (pages[i + StartPage].Image is not null)
                {
                    g.DrawImage(pages[i + StartPage].Image, box);
                }

                box.Width--;
                box.Height--;
                g.DrawRectangle(Pens.Black, box);
            }
        }
    }

    private void PaintResizeBox(PaintEventArgs e)
    {
        if (!_hScrollBar.Visible || !_vScrollBar.Visible)
        {
            return;
        }

        e.Graphics.FillRectangle(SystemBrushes.Control, ResizeBoxRectangle);
    }

    private void PaintFocus(PaintEventArgs e, bool isHighContrast)
    {
        Rectangle focusRect = FocusRectangle;

        if (!e.ClipRectangle.Contains(focusRect) || !Focused || !ShowFocusCues)
        {
            return;
        }

        if (isHighContrast)
        {
            ControlPaint.DrawHighContrastFocusRectangle(e.Graphics, focusRect, SystemColors.ControlText);
        }
        else
        {
            ControlPaint.DrawFocusRectangle(e.Graphics, focusRect);
        }
    }

    /// <summary>
    ///  Gets back color respectively to the High Contrast theme is applied or not
    ///  and taking into account saved custom back color.
    /// </summary>
    /// <param name="isHighContract">Indicates whether High Contrast theme is applied or not.</param>
    /// <returns>
    ///  Standard back color for PrintPreview control in standard theme (1),
    ///  contrasted color if there is High Contrast theme applied (2) and
    ///  custom color if this is set irrespectively to HC or not HC mode (3).
    /// </returns>
    private Color GetBackColor(bool isHighContract)
    {
        return (isHighContract && !ShouldSerializeBackColor()) ? SystemColors.ControlDarkDark : BackColor;
    }

    private static int PixelsToPhysical(int pixels, int dpi) => (int)(pixels * 100.0 / dpi);

    private static Size PixelsToPhysical(Size pixels, Point dpi) =>
        new(PixelsToPhysical(pixels.Width, dpi.X), PixelsToPhysical(pixels.Height, dpi.Y));

    private static int PhysicalToPixels(int physicalSize, int dpi) =>
        (int)(physicalSize * dpi / 100.0);

    private static Size PhysicalToPixels(Size physical, Point dpi) =>
        new(PhysicalToPixels(physical.Width, dpi.X), PhysicalToPixels(physical.Height, dpi.Y));

    private void SetPositionNoInvalidate(Point value)
    {
        _scrollLayoutPending = true;

        _hScrollBar.Value = Math.Clamp(value.X, _hScrollBar.Minimum, _hScrollBar.Maximum);
        _vScrollBar.Value = Math.Clamp(value.Y, _vScrollBar.Minimum, _vScrollBar.Maximum);

        _scrollLayoutPending = false;
    }

    internal void SetVirtualSizeNoInvalidate(Size value)
    {
        if (!IsHandleCreated)
        {
            return;
        }

        _virtualSize = value;

        LayoutScrollBars();
    }

    private void LayoutScrollBars()
    {
        if (!IsHandleCreated)
        {
            return;
        }

        _scrollLayoutPending = true;
        SuspendLayout();

        Rectangle availableRect = InnerClientRectangle;

        (bool horizontalScrollNeeded, bool verticalScrollNeeded) = IsScrollNeeded(availableRect.Size);

        bool scrollBarsVisibilityChanged = (_vScrollBar.Visible ^ verticalScrollNeeded) || (_hScrollBar.Visible ^ horizontalScrollNeeded);

        _hScrollBar.Visible = horizontalScrollNeeded;
        if (horizontalScrollNeeded)
        {
            _hScrollBar.Top = availableRect.Bottom - _hScrollBar.Height;
            _hScrollBar.Width = availableRect.Width - (verticalScrollNeeded ? _vScrollBar.Width : 0);

            AdjustScroll(
                _hScrollBar,
                virtualDimension: _virtualSize.Width,
                displayDimension: availableRect.Width,
                offset: (verticalScrollNeeded ? _vScrollBar.Width : 0));
        }
        else if (_hScrollBar.Value > 0)
        {
            _hScrollBar.Value = 0;
        }

        _vScrollBar.Visible = verticalScrollNeeded;
        if (verticalScrollNeeded)
        {
            _vScrollBar.Height = availableRect.Height - (horizontalScrollNeeded ? _hScrollBar.Height : 0);

            if (RightToLeft == RightToLeft.Yes)
            {
                _vScrollBar.Left = availableRect.Left;
                _vScrollBar.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Bottom;
            }
            else
            {
                _vScrollBar.Left = availableRect.Right - _vScrollBar.Width;
                _vScrollBar.Anchor = AnchorStyles.Top | AnchorStyles.Right | AnchorStyles.Bottom;
            }

            AdjustScroll(
                _vScrollBar,
                virtualDimension: _virtualSize.Height,
                displayDimension: availableRect.Height,
                offset: (horizontalScrollNeeded ? _hScrollBar.Height : 0));
        }
        else if (_vScrollBar.Value > 0)
        {
            _vScrollBar.Value = 0;
        }

        ResumeLayout(true);
        _scrollLayoutPending = false;

        if (scrollBarsVisibilityChanged
            && IsAccessibilityObjectCreated
            && AccessibilityObject is PrintPreviewControlAccessibleObject ao)
        {
            ao.RaiseStructureChangedEvent(StructureChangeType.StructureChangeType_ChildrenInvalidated, []);
        }

        (bool horizontal, bool vertical) IsScrollNeeded(Size displaySize)
        {
            bool horizontal = _virtualSize.Width > displaySize.Width && displaySize.Width > _vScrollBar.Width;
            bool vertical = _virtualSize.Height > displaySize.Height && displaySize.Height > _hScrollBar.Height;

            if (!horizontal && vertical)
            {
                horizontal = _virtualSize.Width > (displaySize.Width - _vScrollBar.Width);
            }

            if (!vertical && horizontal)
            {
                vertical = _virtualSize.Height > (displaySize.Height - _hScrollBar.Height);
            }

            return (horizontal, vertical);
        }

        void AdjustScroll(ScrollBar scrollBar, int virtualDimension, int displayDimension, int offset)
        {
            int oldLargeChange = scrollBar.LargeChange;

            scrollBar.Maximum = virtualDimension;
            scrollBar.LargeChange = displayDimension - offset;

            if (scrollBar.Value > 0)
            {
                int diff = scrollBar.LargeChange - oldLargeChange;

                if (scrollBar.Value >= diff)
                {
                    scrollBar.Value -= diff;
                }
            }
        }
    }

    private void scrollBar_ValueChanged(object? sender, EventArgs e)
    {
        if (_scrollLayoutPending)
        {
            return;
        }

        Invalidate(InsideRectangle);
    }

    /// <summary>
    ///  Handles the WM_KEYDOWN message.
    /// </summary>
    private void WmKeyDown(ref Message msg)
    {
        Keys keyData = (Keys)(nint)msg.WParamInternal | ModifierKeys;
        Point locPos = Position;
        int pos;
        int maxPos;
        bool scrollVisible = _vScrollBar.Visible || _hScrollBar.Visible;
        bool vertical = _vScrollBar.Visible;
        int largeChange = vertical ? _vScrollBar.LargeChange : _hScrollBar.LargeChange;

        switch (keyData & Keys.KeyCode)
        {
            case Keys.PageUp:
                if ((keyData & Keys.Modifiers) == Keys.Control)
                {
                    if (scrollVisible)
                    {
                        pos = vertical ? locPos.Y : locPos.X;
                        if (pos > largeChange)
                        {
                            pos -= largeChange;
                        }
                        else
                        {
                            pos = 0;
                        }

                        if (vertical)
                        {
                            locPos.Y = pos;
                        }
                        else
                        {
                            locPos.X = pos;
                        }

                        Position = locPos;
                    }
                }
                else if (StartPage > 0)
                {
                    StartPage--;
                }

                break;
            case Keys.PageDown:
                if ((keyData & Keys.Modifiers) == Keys.Control)
                {
                    if (scrollVisible)
                    {
                        pos = vertical ? locPos.Y : locPos.X;
                        maxPos = vertical ? _vScrollBar.Maximum : _hScrollBar.Maximum;
                        if (pos < maxPos - largeChange)
                        {
                            pos += largeChange;
                        }
                        else
                        {
                            pos = maxPos;
                        }

                        if (vertical)
                        {
                            locPos.Y = pos;
                        }
                        else
                        {
                            locPos.X = pos;
                        }

                        Position = locPos;
                    }
                }
                else if (_pageInfo is not null && StartPage < _pageInfo.Length)
                {
                    StartPage++;
                }

                break;
            case Keys.Home:
                if ((keyData & Keys.Modifiers) == Keys.Control)
                {
                    StartPage = 0;
                }

                break;
            case Keys.End:
                if (_pageInfo is not null && (keyData & Keys.Modifiers) == Keys.Control)
                {
                    StartPage = _pageInfo.Length;
                }

                break;

            case Keys.Up:
                if (_vScrollBar.Visible)
                {
                    pos = locPos.Y;
                    if (pos > _vScrollBar.SmallChange)
                    {
                        pos -= _vScrollBar.SmallChange;
                    }
                    else
                    {
                        pos = 0;
                    }

                    locPos.Y = pos;
                    Position = locPos;
                }

                break;
            case Keys.Down:
                if (_vScrollBar.Visible)
                {
                    pos = locPos.Y;
                    maxPos = _vScrollBar.Maximum - _vScrollBar.SmallChange;

                    if (pos < maxPos - _vScrollBar.SmallChange)
                    {
                        pos += _vScrollBar.SmallChange;
                    }
                    else
                    {
                        pos = maxPos;
                    }

                    locPos.Y = pos;
                    Position = locPos;
                }

                break;
            case Keys.Left:
                if (_hScrollBar.Visible)
                {
                    pos = locPos.X;
                    if (pos > _hScrollBar.SmallChange)
                    {
                        pos -= _hScrollBar.SmallChange;
                    }
                    else
                    {
                        pos = 0;
                    }

                    locPos.X = pos;
                    Position = locPos;
                }

                break;
            case Keys.Right:
                if (_hScrollBar.Visible)
                {
                    pos = locPos.X;
                    maxPos = _hScrollBar.Maximum - _hScrollBar.SmallChange;

                    if (pos < maxPos - _hScrollBar.SmallChange)
                    {
                        pos += _hScrollBar.SmallChange;
                    }
                    else
                    {
                        pos = maxPos;
                    }

                    locPos.X = pos;
                    Position = locPos;
                }

                break;
        }
    }

    protected override void WndProc(ref Message m)
    {
        switch (m.MsgInternal)
        {
            case PInvokeCore.WM_KEYDOWN:
                WmKeyDown(ref m);
                break;
            default:
                base.WndProc(ref m);
                break;
        }
    }
}
