// <copyright file="FirefoxOptions.cs" company="WebDriver 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;

namespace OpenQA.Selenium.Firefox
{
    /// <summary>
    /// Class to manage options specific to <see cref="FirefoxDriver"/>
    /// </summary>
    /// <remarks>
    /// Used with the marionette executable wires.exe.
    /// </remarks>
    /// <example>
    /// <code>
    /// FirefoxOptions options = new FirefoxOptions();
    /// </code>
    /// <para></para>
    /// <para>For use with FirefoxDriver:</para>
    /// <para></para>
    /// <code>
    /// FirefoxDriver driver = new FirefoxDriver(options);
    /// </code>
    /// <para></para>
    /// <para>For use with RemoteWebDriver:</para>
    /// <para></para>
    /// <code>
    /// RemoteWebDriver driver = new RemoteWebDriver(new Uri("http://localhost:4444/wd/hub"), options.ToCapabilities());
    /// </code>
    /// </example>
    public class FirefoxOptions : DriverOptions
    {
        private const string BrowserNameValue = "firefox";

        private const string IsMarionetteCapability = "marionette";
        private const string FirefoxLegacyProfileCapability = "firefox_profile";
        private const string FirefoxLegacyBinaryCapability = "firefox_binary";
        private const string FirefoxProfileCapability = "profile";
        private const string FirefoxBinaryCapability = "binary";
        private const string FirefoxArgumentsCapability = "args";
        private const string FirefoxLogCapability = "log";
        private const string FirefoxPrefsCapability = "prefs";
        private const string FirefoxEnvCapability = "env";
        private const string FirefoxOptionsCapability = "moz:firefoxOptions";
        private const string FirefoxEnableDevToolsProtocolCapability = "moz:debuggerAddress";

        private bool enableDevToolsProtocol;
        private string binaryLocation;
        private FirefoxDriverLogLevel logLevel = FirefoxDriverLogLevel.Default;
        private FirefoxProfile profile;
        private List<string> firefoxArguments = new List<string>();
        private Dictionary<string, object> profilePreferences = new Dictionary<string, object>();
        private Dictionary<string, object> additionalFirefoxOptions = new Dictionary<string, object>();
        private Dictionary<string, object> environmentVariables = new Dictionary<string, object>();
        private FirefoxAndroidOptions androidOptions;

        /// <summary>
        /// Initializes a new instance of the <see cref="FirefoxOptions"/> class.
        /// </summary>
        public FirefoxOptions()
            : base()
        {
            this.BrowserName = BrowserNameValue;
            this.AddKnownCapabilityName(FirefoxOptions.FirefoxOptionsCapability, "current FirefoxOptions class instance");
            this.AddKnownCapabilityName(FirefoxOptions.IsMarionetteCapability, "UseLegacyImplementation property");
            this.AddKnownCapabilityName(FirefoxOptions.FirefoxProfileCapability, "Profile property");
            this.AddKnownCapabilityName(FirefoxOptions.FirefoxBinaryCapability, "BrowserExecutableLocation property");
            this.AddKnownCapabilityName(FirefoxOptions.FirefoxArgumentsCapability, "AddArguments method");
            this.AddKnownCapabilityName(FirefoxOptions.FirefoxPrefsCapability, "SetPreference method");
            this.AddKnownCapabilityName(FirefoxOptions.FirefoxEnvCapability, "SetEnvironmentVariable method");
            this.AddKnownCapabilityName(FirefoxOptions.FirefoxLogCapability, "LogLevel property");
            this.AddKnownCapabilityName(FirefoxOptions.FirefoxLegacyProfileCapability, "Profile property");
            this.AddKnownCapabilityName(FirefoxOptions.FirefoxLegacyBinaryCapability, "BrowserExecutableLocation property");
            this.AddKnownCapabilityName(FirefoxOptions.FirefoxEnableDevToolsProtocolCapability, "EnableDevToolsProtocol property");
            // Firefox 129 onwards the CDP protocol will not be enabled by default. Setting this preference will enable it.
            // https://fxdx.dev/deprecating-cdp-support-in-firefox-embracing-the-future-with-webdriver-bidi/.
            this.SetPreference("remote.active-protocols", 3);
        }

        /// <summary>
        /// Gets or sets the <see cref="FirefoxProfile"/> object to be used with this instance.
        /// </summary>
        public FirefoxProfile Profile
        {
            get { return this.profile; }
            set { this.profile = value; }
        }

        /// <summary>
        /// Gets or sets the path and file name of the Firefox browser executable.
        /// </summary>
        public override string BinaryLocation
        {
            get { return this.binaryLocation; }
            set { this.binaryLocation = value; }
        }

        /// <summary>
        /// Gets or sets the path and file name of the Firefox browser executable.
        /// </summary>
        [Obsolete("Use BinaryLocation property instead of BrowserExecutableLocation. This one will be removed soon.")]
        public string BrowserExecutableLocation
        {
            get { return this.binaryLocation; }
            set { this.binaryLocation = value; }
        }

        /// <summary>
        /// Gets or sets the logging level of the Firefox driver.
        /// </summary>
        public FirefoxDriverLogLevel LogLevel
        {
            get { return this.logLevel; }
            set { this.logLevel = value; }
        }

        /// <summary>
        /// Gets or sets a value indicating whether to enable the DevTools protocol for the launched browser.
        /// </summary>
        public bool EnableDevToolsProtocol
        {
            get { return this.enableDevToolsProtocol; }
            set { this.enableDevToolsProtocol = value; }
        }

        /// <summary>
        /// Gets or sets the options for automating Firefox on Android.
        /// </summary>
        public FirefoxAndroidOptions AndroidOptions
        {
            get { return this.androidOptions; }
            set { this.androidOptions = value; }
        }

        /// <summary>
        /// Adds an argument to be used in launching the Firefox browser.
        /// </summary>
        /// <param name="argumentName">The argument to add.</param>
        /// <remarks>Arguments must be preceeded by two dashes ("--").</remarks>
        public void AddArgument(string argumentName)
        {
            if (string.IsNullOrEmpty(argumentName))
            {
                throw new ArgumentException("argumentName must not be null or empty", nameof(argumentName));
            }

            this.AddArguments(argumentName);
        }

        /// <summary>
        /// Adds a list arguments to be used in launching the Firefox browser.
        /// </summary>
        /// <param name="argumentsToAdd">An array of arguments to add.</param>
        /// <remarks>Each argument must be preceeded by two dashes ("--").</remarks>
        public void AddArguments(params string[] argumentsToAdd)
        {
            this.AddArguments(new List<string>(argumentsToAdd));
        }

        /// <summary>
        /// Adds a list arguments to be used in launching the Firefox browser.
        /// </summary>
        /// <param name="argumentsToAdd">An array of arguments to add.</param>
        public void AddArguments(IEnumerable<string> argumentsToAdd)
        {
            if (argumentsToAdd == null)
            {
                throw new ArgumentNullException(nameof(argumentsToAdd), "argumentsToAdd must not be null");
            }

            this.firefoxArguments.AddRange(argumentsToAdd);
        }

        /// <summary>
        /// Sets a preference in the profile used by Firefox.
        /// </summary>
        /// <param name="preferenceName">Name of the preference to set.</param>
        /// <param name="preferenceValue">Value of the preference to set.</param>
        public void SetPreference(string preferenceName, bool preferenceValue)
        {
            this.SetPreferenceValue(preferenceName, preferenceValue);
        }

        /// <summary>
        /// Sets a preference in the profile used by Firefox.
        /// </summary>
        /// <param name="preferenceName">Name of the preference to set.</param>
        /// <param name="preferenceValue">Value of the preference to set.</param>
        public void SetPreference(string preferenceName, int preferenceValue)
        {
            this.SetPreferenceValue(preferenceName, preferenceValue);
        }

        /// <summary>
        /// Sets a preference in the profile used by Firefox.
        /// </summary>
        /// <param name="preferenceName">Name of the preference to set.</param>
        /// <param name="preferenceValue">Value of the preference to set.</param>
        public void SetPreference(string preferenceName, long preferenceValue)
        {
            this.SetPreferenceValue(preferenceName, preferenceValue);
        }

        /// <summary>
        /// Sets a preference in the profile used by Firefox.
        /// </summary>
        /// <param name="preferenceName">Name of the preference to set.</param>
        /// <param name="preferenceValue">Value of the preference to set.</param>
        public void SetPreference(string preferenceName, double preferenceValue)
        {
            this.SetPreferenceValue(preferenceName, preferenceValue);
        }

        /// <summary>
        /// Sets a preference in the profile used by Firefox.
        /// </summary>
        /// <param name="preferenceName">Name of the preference to set.</param>
        /// <param name="preferenceValue">Value of the preference to set.</param>
        public void SetPreference(string preferenceName, string preferenceValue)
        {
            this.SetPreferenceValue(preferenceName, preferenceValue);
        }

        /// <summary>
        /// Sets an environment variable to be set in the operating system's environment under which the Firerox browser is launched.
        /// </summary>
        /// <param name="variableName">The name of the environment variable.</param>
        /// <param name="variableValue">The value of the environment variable.</param>
        public void SetEnvironmentVariable(string variableName, string variableValue)
        {
            if (string.IsNullOrEmpty(variableName))
            {
                throw new ArgumentException("Environment variable name cannot be null or an empty string");
            }

            if (variableValue == null)
            {
                variableValue = string.Empty;
            }

            this.environmentVariables[variableName] = variableValue;
        }

        /// <summary>
        /// Provides a means to add additional capabilities not yet added as type safe options
        /// for the Firefox driver.
        /// </summary>
        /// <param name="optionName">The name of the capability to add.</param>
        /// <param name="optionValue">The value of the capability to add.</param>
        /// <exception cref="ArgumentException">
        /// thrown when attempting to add a capability for which there is already a type safe option, or
        /// when <paramref name="optionName"/> is <see langword="null"/> or the empty string.
        /// </exception>
        /// <remarks>Calling <see cref="AddAdditionalFirefoxOption(string, object)"/>
        /// where <paramref name="optionName"/> has already been added will overwrite the
        /// existing value with the new value in <paramref name="optionValue"/>.
        /// Calling this method adds capabilities to the Firefox-specific options object passed to
        /// geckodriver.exe (property name 'moz:firefoxOptions').</remarks>
        public void AddAdditionalFirefoxOption(string optionName, object optionValue)
        {
            this.ValidateCapabilityName(optionName);
            this.additionalFirefoxOptions[optionName] = optionValue;
        }

        /// <summary>
        /// Returns DesiredCapabilities for Firefox with these options included as
        /// capabilities. This does not copy the options. Further changes will be
        /// reflected in the returned capabilities.
        /// </summary>
        /// <returns>The DesiredCapabilities for Firefox with these options.</returns>
        public override ICapabilities ToCapabilities()
        {
            IWritableCapabilities capabilities = GenerateDesiredCapabilities(true);
            Dictionary<string, object> firefoxOptions = this.GenerateFirefoxOptionsDictionary();
            capabilities.SetCapability(FirefoxOptionsCapability, firefoxOptions);
            if (this.enableDevToolsProtocol)
            {
                capabilities.SetCapability(FirefoxEnableDevToolsProtocolCapability, true);
            }

            return capabilities.AsReadOnly();
        }

        private Dictionary<string, object> GenerateFirefoxOptionsDictionary()
        {
            Dictionary<string, object> firefoxOptions = new Dictionary<string, object>();

            if (this.profile != null)
            {
                firefoxOptions[FirefoxProfileCapability] = this.profile.ToBase64String();
            }

            if (!string.IsNullOrEmpty(this.binaryLocation))
            {
                firefoxOptions[FirefoxBinaryCapability] = this.binaryLocation;
            }

            if (this.logLevel != FirefoxDriverLogLevel.Default)
            {
                Dictionary<string, object> logObject = new Dictionary<string, object>();
                logObject["level"] = this.logLevel.ToString().ToLowerInvariant();
                firefoxOptions[FirefoxLogCapability] = logObject;
            }

            if (this.firefoxArguments.Count > 0)
            {
                List<object> args = new List<object>();
                foreach (string argument in this.firefoxArguments)
                {
                    args.Add(argument);
                }

                firefoxOptions[FirefoxArgumentsCapability] = args;
            }

            if (this.profilePreferences.Count > 0)
            {
                firefoxOptions[FirefoxPrefsCapability] = this.profilePreferences;
            }

            if (this.environmentVariables.Count > 0)
            {
                firefoxOptions[FirefoxEnvCapability] = this.environmentVariables;
            }

            if (this.androidOptions != null)
            {
                this.AddAndroidOptions(firefoxOptions);
            }

            foreach (KeyValuePair<string, object> pair in this.additionalFirefoxOptions)
            {
                firefoxOptions.Add(pair.Key, pair.Value);
            }

            return firefoxOptions;
        }

        private void SetPreferenceValue(string preferenceName, object preferenceValue)
        {
            if (string.IsNullOrEmpty(preferenceName))
            {
                throw new ArgumentException("Preference name may not be null an empty string.", nameof(preferenceName));
            }

            this.profilePreferences[preferenceName] = preferenceValue;
        }

        private void AddAndroidOptions(Dictionary<string, object> firefoxOptions)
        {
            firefoxOptions["androidPackage"] = this.androidOptions.AndroidPackage;

            if (!string.IsNullOrEmpty(this.androidOptions.AndroidDeviceSerial))
            {
                firefoxOptions["androidDeviceSerial"] = this.androidOptions.AndroidDeviceSerial;
            }

            if (!string.IsNullOrEmpty(this.androidOptions.AndroidActivity))
            {
                firefoxOptions["androidActivity"] = this.androidOptions.AndroidActivity;
            }

            if (this.androidOptions.AndroidIntentArguments.Count > 0)
            {
                List<object> args = new List<object>();
                foreach (string argument in this.androidOptions.AndroidIntentArguments)
                {
                    args.Add(argument);
                }

                firefoxOptions["androidIntentArguments"] = args;
            }
        }
    }
}
