// <copyright file="PrintOptions.cs" company="Selenium Committers">
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.
// </copyright>

using System;
using System.Collections.Generic;
using System.Globalization;

namespace OpenQA.Selenium;

/// <summary>
/// Represents the orientation of the page in the printed document.
/// </summary>
public enum PrintOrientation
{
    /// <summary>
    /// Print the document in portrait mode.
    /// </summary>
    Portrait,

    /// <summary>
    /// Print the document in landscape mode.
    /// </summary>
    Landscape
}

/// <summary>
/// Represents the options to send for printing a page.
/// </summary>
public class PrintOptions
{
    private const double DefaultMarginSize = 1.0;
    private const double DefaultPageHeight = 21.59;
    private const double DefaultPageWidth = 27.94;
    private const double CentimetersPerInch = 2.54;

    private double scale = 1.0;
    private PageSize pageSize = new PageSize();
    private Margins margins = new Margins();
    private readonly HashSet<object> pageRanges = new HashSet<object>();

    /// <summary>
    /// Gets or sets the orientation of the pages in the printed document.
    /// </summary>
    public PrintOrientation Orientation { get; set; } = PrintOrientation.Portrait;

    /// <summary>
    /// Gets or sets the amount which the printed content is zoomed. Valid values are 0.1 to 2.0.
    /// </summary>
    /// <exception cref="ArgumentOutOfRangeException">If the value is not set between 0.1 and 2.0.</exception>
    public double ScaleFactor
    {
        get => this.scale;
        set
        {
            if (value < 0.1 || value > 2.0)
            {
                throw new ArgumentOutOfRangeException(nameof(value), "Scale factor must be between 0.1 and 2.0.");
            }

            this.scale = value;
        }
    }

    /// <summary>
    /// Gets or sets a value indicating whether to print background images in the printed document.
    /// </summary>
    public bool OutputBackgroundImages { get; set; }

    /// <summary>
    /// Gets or sets a value indicating whether to shrink the content to fit the printed page size.
    /// </summary>
    public bool ShrinkToFit { get; set; } = true;

    /// <summary>
    /// Gets or sets the dimensions for each page in the printed document.
    /// </summary>
    /// <exception cref="ArgumentNullException">If the value is set to <see langword="null"/>.</exception>
    public PageSize PageDimensions
    {
        get => this.pageSize;
        set => this.pageSize = value ?? throw new ArgumentNullException(nameof(value));
    }

    /// <summary>
    /// Gets or sets the margins for each page in the doucment.
    /// </summary>
    /// <exception cref="ArgumentNullException">If the value is set to <see langword="null"/>.</exception>
    public Margins PageMargins
    {
        get => this.margins;
        set => this.margins = value ?? throw new ArgumentNullException(nameof(value));
    }

    /// <summary>
    /// Adds a page to the list of pages to be included in the document.
    /// </summary>
    /// <param name="pageNumber">The page number to be included in the document.</param>
    /// <exception cref="ArgumentOutOfRangeException">If <paramref name="pageNumber"/> is negative.</exception>
    /// <exception cref="ArgumentException">If the requested page has already been added.</exception>
    public void AddPageToPrint(int pageNumber)
    {
        if (pageNumber < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(pageNumber), "Page number must be greater than or equal to zero");
        }

        if (this.pageRanges.Contains(pageNumber))
        {
            throw new ArgumentException("Cannot add the same page number twice", nameof(pageNumber));
        }

        this.pageRanges.Add(pageNumber);
    }

    /// <summary>
    /// Adds a range of pages to be included in the document.
    /// </summary>
    /// <param name="pageRange">A string of the form "x-y" representing the page numbers to include.</param>
    /// <exception cref="ArgumentException">
    /// <para>If <paramref name="pageRange"/> is <see langword="null"/> or <see cref="string.Empty"/>.</para>
    /// <para>-or-</para>
    /// <para>If the requested <paramref name="pageRange"/> is already included.</para>
    /// <para>-or-</para>
    /// <para>If the requested <paramref name="pageRange"/> has multiple '-' separators.</para>
    /// <para>-or-</para>
    /// <para>If a bound value is neither empty nor a number.</para>
    /// </exception>
    /// <exception cref="ArgumentOutOfRangeException">
    /// <para>If <paramref name="pageRange"/> has a negative lower bound.</para>
    /// <para>-or-</para>
    /// <para>If <paramref name="pageRange"/> has an upper bound less than the lower bound.</para>
    /// </exception>
    public void AddPageRangeToPrint(string pageRange)
    {
        if (string.IsNullOrEmpty(pageRange))
        {
            throw new ArgumentException("Page range cannot be null or the empty string", nameof(pageRange));
        }

        if (this.pageRanges.Contains(pageRange))
        {
            throw new ArgumentException("Cannot add the same page range twice", nameof(pageRange));
        }

        string[] pageRangeParts = pageRange.Trim().Split('-');
        if (pageRangeParts.Length > 2)
        {
            throw new ArgumentException("Page range cannot have multiple separators", nameof(pageRange));
        }

        int startPage = ParsePageRangePart(pageRangeParts[0], 1);
        if (startPage < 1)
        {
            throw new ArgumentOutOfRangeException(nameof(pageRange), "Start of a page range must be greater than or equal to 1");
        }

        if (pageRangeParts.Length == 2)
        {
            int endPage = ParsePageRangePart(pageRangeParts[1], int.MaxValue);
            if (endPage < startPage)
            {
                throw new ArgumentOutOfRangeException(nameof(pageRange), "End of a page range must be greater than or equal to the start of the page range");
            }
        }

        this.pageRanges.Add(pageRange);
    }

    internal Dictionary<string, object?> ToDictionary()
    {
        Dictionary<string, object?> toReturn = new Dictionary<string, object?>();

        if (this.Orientation != PrintOrientation.Portrait)
        {
            toReturn["orientation"] = this.Orientation.ToString().ToLowerInvariant();
        }

        if (this.scale != 1.0)
        {
            toReturn["scale"] = this.scale;
        }

        if (this.OutputBackgroundImages)
        {
            toReturn["background"] = this.OutputBackgroundImages;
        }

        if (!this.ShrinkToFit)
        {
            toReturn["shrinkToFit"] = this.ShrinkToFit;
        }

        if (this.pageSize.Height != DefaultPageHeight || this.pageSize.Width != DefaultPageWidth)
        {
            Dictionary<string, object?> pageSizeDictionary = new Dictionary<string, object?>();
            pageSizeDictionary["width"] = this.pageSize.Width;
            pageSizeDictionary["height"] = this.pageSize.Height;
            toReturn["page"] = pageSizeDictionary;
        }

        if (this.margins.Top != DefaultMarginSize || this.margins.Bottom != DefaultMarginSize || this.margins.Left != DefaultMarginSize || this.margins.Right != DefaultMarginSize)
        {
            Dictionary<string, object?> marginsDictionary = new Dictionary<string, object?>();
            marginsDictionary["top"] = this.margins.Top;
            marginsDictionary["bottom"] = this.margins.Bottom;
            marginsDictionary["left"] = this.margins.Left;
            marginsDictionary["right"] = this.margins.Right;
            toReturn["margin"] = marginsDictionary;
        }

        if (this.pageRanges.Count > 0)
        {
            toReturn["pageRanges"] = new List<object?>(this.pageRanges);
        }

        return toReturn;
    }

    private static int ParsePageRangePart(string pageRangePart, int defaultValue)
    {
        pageRangePart = pageRangePart.Trim();

        if (string.IsNullOrEmpty(pageRangePart))
        {
            return defaultValue;
        }

        if (int.TryParse(pageRangePart, NumberStyles.Integer, CultureInfo.InvariantCulture, out int pageRangePartValue))
        {
            return pageRangePartValue;
        }

        throw new ArgumentException("Parts of a page range must be an empty string or an integer");
    }

    /// <summary>
    /// An object representing the page size of the print options.
    /// </summary>
    public class PageSize
    {
        private double height = DefaultPageHeight;
        private double width = DefaultPageWidth;

        /// <summary>
        /// Represents the A4 paper size.
        /// Width: 21.0 cm, Height: 29.7 cm
        /// </summary>
        public static PageSize A4 => new PageSize { Width = 21.0, Height = 29.7 }; // cm

        /// <summary>
        /// Represents the Legal paper size.
        /// Width: 21.59 cm, Height: 35.56 cm
        /// </summary>
        public static PageSize Legal => new PageSize { Width = 21.59, Height = 35.56 }; // cm

        /// <summary>
        /// Represents the Letter paper size.
        /// Width: 21.59 cm, Height: 27.94 cm
        /// </summary>
        public static PageSize Letter => new PageSize { Width = 21.59, Height = 27.94 }; // cm

        /// <summary>
        /// Represents the Tabloid paper size.
        /// Width: 27.94 cm, Height: 43.18 cm
        /// </summary>
        public static PageSize Tabloid => new PageSize { Width = 27.94, Height = 43.18 }; // cm

        /// <summary>
        /// Gets or sets the height of each page in centimeters.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">If the value is set to a negative value.</exception>
        public double Height
        {
            get => this.height;
            set
            {
                if (value < 0)
                {
                    throw new ArgumentOutOfRangeException(nameof(value), "Height must be greater than or equal to zero.");
                }

                this.height = value;
            }
        }

        /// <summary>
        /// Gets or sets the width of each page in centimeters.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">If the value is set to a negative value.</exception>
        public double Width
        {
            get => this.width;
            set
            {
                if (value < 0)
                {
                    throw new ArgumentOutOfRangeException(nameof(value), "Width must be greater than or equal to zero.");
                }

                this.width = value;
            }
        }

        /// <summary>
        /// Gets or sets the height of each page in inches.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">If the value is set to a negative value.</exception>
        public double HeightInInches
        {
            get => this.Height / CentimetersPerInch;
            set => this.Height = value * CentimetersPerInch;
        }

        /// <summary>
        /// Gets or sets the width of each page in inches.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">If the value is set to a negative value.</exception>
        public double WidthInInches
        {
            get => this.Width / CentimetersPerInch;
            set => this.Width = value * CentimetersPerInch;
        }
    }

    /// <summary>
    /// An object representing the margins for printing.
    /// </summary>
    public class Margins
    {
        private double top = DefaultMarginSize;
        private double bottom = DefaultMarginSize;
        private double left = DefaultMarginSize;
        private double right = DefaultMarginSize;

        /// <summary>
        /// Gets or sets the top margin of the print options.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">If the value is set to a negative value.</exception>
        public double Top
        {
            get => this.top;
            set
            {
                if (value < 0)
                {
                    throw new ArgumentOutOfRangeException(nameof(value), "Top margin must be greater than or equal to zero.");
                }

                this.top = value;
            }
        }

        /// <summary>
        /// Gets or sets the bottom margin of the print options.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">If the value is set to a negative value.</exception>
        public double Bottom
        {
            get => this.bottom;
            set
            {
                if (value < 0)
                {
                    throw new ArgumentOutOfRangeException(nameof(value), "Bottom margin must be greater than or equal to zero.");
                }

                this.bottom = value;
            }
        }

        /// <summary>
        /// Gets or sets the left margin of the print options.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">If the value is set to a negative value.</exception>
        public double Left
        {
            get => this.left;
            set
            {
                if (value < 0)
                {
                    throw new ArgumentOutOfRangeException(nameof(value), "Left margin must be greater than or equal to zero.");
                }

                this.left = value;
            }
        }

        /// <summary>
        /// Gets or sets the right margin of the print options.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">If the value is set to a negative value.</exception>
        public double Right
        {
            get => this.right;
            set
            {
                if (value < 0)
                {
                    throw new ArgumentOutOfRangeException(nameof(value), "Right margin must be greater than or equal to zero.");
                }

                this.right = value;
            }
        }
    }
}
