// <copyright file="FirefoxExtension.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 OpenQA.Selenium.Internal;
using System;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Text.Json.Nodes;
using System.Xml;

namespace OpenQA.Selenium.Firefox;

/// <summary>
/// Provides the ability to install extensions into a <see cref="FirefoxProfile"/>.
/// </summary>
public class FirefoxExtension
{
    private const string EmNamespaceUri = "http://www.mozilla.org/2004/em-rdf#";
    private const string RdfManifestFileName = "install.rdf";
    private const string JsonManifestFileName = "manifest.json";

    private readonly string extensionFileName;
    private readonly string extensionResourceId;

    /// <summary>
    /// Initializes a new instance of the <see cref="FirefoxExtension"/> class.
    /// </summary>
    /// <param name="fileName">The name of the file containing the Firefox extension.</param>
    /// <remarks>WebDriver attempts to resolve the <paramref name="fileName"/> parameter
    /// by looking first for the specified file in the directory of the calling assembly,
    /// then using the full path to the file, if a full path is provided.</remarks>
    /// <exception cref="ArgumentNullException">If <paramref name="fileName"/> is <see langword="null"/>.</exception>
    public FirefoxExtension(string fileName)
        : this(fileName, string.Empty)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="FirefoxExtension"/> class.
    /// </summary>
    /// <param name="fileName">The name of the file containing the Firefox extension.</param>
    /// <param name="resourceId">The ID of the resource within the assembly containing the extension
    /// if the file is not present in the file system.</param>
    /// <remarks>WebDriver attempts to resolve the <paramref name="fileName"/> parameter
    /// by looking first for the specified file in the directory of the calling assembly,
    /// then using the full path to the file, if a full path is provided. If the file is
    /// not found in the file system, WebDriver attempts to locate a resource in the
    /// executing assembly with the name specified by the <paramref name="resourceId"/>
    /// parameter.</remarks>
    /// <exception cref="ArgumentNullException">If <paramref name="fileName"/> or <paramref name="resourceId"/> are <see langword="null"/>.</exception>
    internal FirefoxExtension(string fileName, string resourceId)
    {
        this.extensionFileName = fileName ?? throw new ArgumentNullException(nameof(fileName));
        this.extensionResourceId = resourceId ?? throw new ArgumentNullException(nameof(resourceId));
    }

    /// <summary>
    /// Installs the extension into a profile directory.
    /// </summary>
    /// <param name="profileDirectory">The Firefox profile directory into which to install the extension.</param>
    /// <exception cref="ArgumentNullException">If <paramref name="profileDirectory"/> is <see langword="null"/>.</exception>
    public void Install(string profileDirectory)
    {
        DirectoryInfo info = new DirectoryInfo(profileDirectory);
        string stagingDirectoryName = Path.Combine(Path.GetTempPath(), info.Name + ".staging");
        string tempFileName = Path.Combine(stagingDirectoryName, Path.GetFileName(this.extensionFileName));
        if (Directory.Exists(tempFileName))
        {
            Directory.Delete(tempFileName, true);
        }

        // First, expand the .xpi archive into a temporary location.
        Directory.CreateDirectory(tempFileName);
        Stream zipFileStream = ResourceUtilities.GetResourceStream(this.extensionFileName, this.extensionResourceId);
        using (ZipArchive extensionZipArchive = new ZipArchive(zipFileStream, ZipArchiveMode.Read))
        {
            extensionZipArchive.ExtractToDirectory(tempFileName);
        }

        // Then, copy the contents of the temporarly location into the
        // proper location in the Firefox profile directory.
        string id = GetExtensionId(tempFileName);
        string extensionDirectory = Path.Combine(Path.Combine(profileDirectory, "extensions"), id);
        if (Directory.Exists(extensionDirectory))
        {
            Directory.Delete(extensionDirectory, true);
        }

        Directory.CreateDirectory(extensionDirectory);
        FileUtilities.CopyDirectory(tempFileName, extensionDirectory);

        // By deleting the staging directory, we also delete the temporarily
        // expanded extension, which we copied into the profile.
        FileUtilities.DeleteDirectory(stagingDirectoryName);
    }

    private static string GetExtensionId(string root)
    {
        // Checks if manifest.json or install.rdf file exists and extracts
        // the addon/extenion id from the file accordingly
        string manifestJsonPath = Path.Combine(root, JsonManifestFileName);
        string installRdfPath = Path.Combine(root, RdfManifestFileName);

        if (File.Exists(installRdfPath))
        {
            return ReadIdFromInstallRdf(root);
        }

        if (File.Exists(manifestJsonPath))
        {
            return ReadIdFromManifestJson(root);
        }

        throw new WebDriverException("Extension should contain either install.rdf or manifest.json metadata file");
    }

    private static string ReadIdFromInstallRdf(string root)
    {
        string id;
        string installRdf = Path.Combine(root, "install.rdf");
        try
        {
            XmlDocument rdfXmlDocument = new XmlDocument();
            rdfXmlDocument.Load(installRdf);

            XmlNamespaceManager rdfNamespaceManager = new XmlNamespaceManager(rdfXmlDocument.NameTable);
            rdfNamespaceManager.AddNamespace("em", EmNamespaceUri);
            rdfNamespaceManager.AddNamespace("RDF", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");

            XmlNode? node = rdfXmlDocument.SelectSingleNode("//em:id", rdfNamespaceManager);
            if (node == null)
            {
                XmlNode? descriptionNode = rdfXmlDocument.SelectSingleNode("//RDF:Description", rdfNamespaceManager);
                XmlAttribute? attribute = descriptionNode?.Attributes?["id", EmNamespaceUri];
                if (attribute == null)
                {
                    throw new WebDriverException("Cannot locate node containing extension id: " + installRdf);
                }

                id = attribute.Value;
            }
            else
            {
                id = node.InnerText;
            }

            if (string.IsNullOrEmpty(id))
            {
                throw new FileNotFoundException("Cannot install extension with ID: " + id);
            }
        }
        catch (Exception e)
        {
            throw new WebDriverException("Error installing extension", e);
        }

        return id;
    }

    private static string ReadIdFromManifestJson(string root)
    {
        string id = string.Empty;
        string manifestJsonPath = Path.Combine(root, JsonManifestFileName);

        var manifestObject = JsonNode.Parse(File.ReadAllText(manifestJsonPath));
        if (manifestObject!["applications"]?["gecko"]?["id"] is { } idNode)
        {
            id = idNode.ToString().Trim();
        }

        if (string.IsNullOrEmpty(id))
        {
            string addInName = manifestObject["name"]!.ToString().Replace(" ", "");
            string addInVersion = manifestObject["version"]!.ToString();
            id = string.Format(CultureInfo.InvariantCulture, "{0}@{1}", addInName, addInVersion);
        }

        return id;
    }
}
