// <copyright file="CommandInfoRepository.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;

#nullable enable

namespace OpenQA.Selenium
{
    /// <summary>
    /// Holds the information about all commands specified by the JSON wire protocol.
    /// This class cannot be inherited, as it is intended to be a singleton, and
    /// allowing subclasses introduces the possibility of multiple instances.
    /// </summary>
    public abstract class CommandInfoRepository
    {
        private readonly Dictionary<string, CommandInfo> commandDictionary;

        /// <summary>
        /// Initializes a new instance of the <see cref="CommandInfoRepository"/> class.
        /// Protected accessibility prevents a default instance from being created.
        /// </summary>
        protected CommandInfoRepository()
        {
            this.commandDictionary = new Dictionary<string, CommandInfo>();
        }

        /// <summary>
        /// Gets the level of the W3C WebDriver specification that this repository supports.
        /// </summary>
        public abstract int SpecificationLevel { get; }

        /// <summary>
        /// Gets the <see cref="Type"/> that is valid for this <see cref="CommandInfoRepository"/>
        /// </summary>
        protected abstract Type RepositoryCommandInfoType { get; }

        /// <summary>
        /// Gets a value indicating whether a command with a given name has been defined.
        /// </summary>
        /// <param name="commandName">The name of the command to check.</param>
        /// <returns><see langword="true"/> if the command name is defined</returns>
        /// <exception cref="ArgumentNullException">If <paramref name="commandName"/> is <see langword="null"/>.</exception>
        public bool IsCommandNameDefined(string commandName)
        {
            return this.commandDictionary.ContainsKey(commandName);
        }

        /// <summary>
        /// Finds a command name for a given <see cref="CommandInfo"/>.
        /// </summary>
        /// <param name="commandInfo">The <see cref="CommandInfo"/> object for which to find the command name.</param>
        /// <returns>The name of the command defined by the command info, or <see langword="null"/> if the command is not defined.</returns>
        public string? FindCommandName(CommandInfo commandInfo)
        {
            foreach (KeyValuePair<string, CommandInfo> pair in this.commandDictionary)
            {
                if (pair.Value == commandInfo)
                {
                    return pair.Key;
                }
            }

            return null;
        }

        /// <summary>
        /// Gets the <see cref="HttpCommandInfo"/> for a <see cref="DriverCommand"/>.
        /// </summary>
        /// <param name="commandName">The <see cref="DriverCommand"/> for which to get the information.</param>
        /// <returns>The <see cref="HttpCommandInfo"/> for the specified command, or <see langword="null"/> if not found or value is not <typeparamref name="T"/>.</returns>
        public T? GetCommandInfo<T>(string commandName) where T : CommandInfo
        {
            T? toReturn = default;
            if (this.commandDictionary.TryGetValue(commandName, out CommandInfo? info))
            {
                toReturn = info as T;
            }

            return toReturn;
        }

        /// <summary>
        /// Tries to add a command to the list of known commands.
        /// </summary>
        /// <param name="commandName">Name of the command.</param>
        /// <param name="commandInfo">The command information.</param>
        /// <returns><see langword="true"/> if the new command has been added successfully; otherwise, <see langword="false"/>.</returns>
        /// <remarks>
        /// This method is used by WebDriver implementations to add additional custom driver-specific commands.
        /// This method will not overwrite existing commands for a specific name, and will return <see langword="false"/>
        /// in that case.
        /// </remarks>
        /// <exception cref="ArgumentNullException">
        /// <para>If <paramref name="commandName"/> is <see langword="null"/> or <see cref="string.Empty"/>.</para>
        /// <para>-or-</para>
        /// <para>If <paramref name="commandInfo"/> is <see langword="null"/>.</para>
        /// </exception>
        /// <exception cref="ArgumentException">If <typeparamref name="T"/> is not a valid command type for this repository.</exception>
        public bool TryAddCommand<T>(string commandName, T commandInfo) where T : CommandInfo
        {
            if (string.IsNullOrEmpty(commandName))
            {
                throw new ArgumentNullException(nameof(commandName), "The name of the command cannot be null or the empty string.");
            }

            if (commandInfo == null)
            {
                throw new ArgumentNullException(nameof(commandInfo), "The command information object cannot be null.");
            }

            if (!typeof(T).IsAssignableFrom(this.RepositoryCommandInfoType))
            {
                string message = string.Format(CultureInfo.InvariantCulture, "{0} is not a valid command type for this repository; command info must be of type {1}", typeof(T), this.RepositoryCommandInfoType);
                throw new ArgumentException(message, nameof(commandInfo));
            }

            if (this.commandDictionary.ContainsKey(commandName))
            {
                return false;
            }

            this.commandDictionary.Add(commandName, commandInfo);
            return true;
        }

        /// <summary>
        /// Initializes the dictionary of commands for the CommandInfoRepository
        /// </summary>
        protected abstract void InitializeCommandDictionary();
    }
}
