// 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.

package org.openqa.selenium.firefox;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.io.FileHandler;
import org.openqa.selenium.io.TemporaryFilesystem;
import org.openqa.selenium.io.Zip;
import org.openqa.selenium.json.Json;

public class FirefoxProfile {

  private static final String ACCEPT_UNTRUSTED_CERTS_PREF = "webdriver_accept_untrusted_certs";
  private static final String ASSUME_UNTRUSTED_ISSUER_PREF = "webdriver_assume_untrusted_issuer";
  private final Preferences additionalPrefs;
  private final Map<String, Extension> extensions = new HashMap<>();
  private final File model;
  private boolean loadNoFocusLib;
  private boolean acceptUntrustedCerts;
  private boolean untrustedCertIssuer;

  public FirefoxProfile() {
    this(null);
  }

  /**
   * Constructs a firefox profile from an existing profile directory.
   *
   * <p>Users who need this functionality should consider using a named profile.
   *
   * @param profileDir The profile directory to use as a model.
   */
  public FirefoxProfile(File profileDir) {
    additionalPrefs = new Preferences();
    model = profileDir;
    verifyModel(model);

    File prefsInModel = new File(model, "user.js");
    if (prefsInModel.exists()) {
      StringReader reader = new StringReader("{\"frozen\": {}, \"mutable\": {}}");
      Preferences existingPrefs = new Preferences(reader, prefsInModel);
      existingPrefs.addTo(this.additionalPrefs);
      acceptUntrustedCerts = getBooleanPreference(existingPrefs, ACCEPT_UNTRUSTED_CERTS_PREF, true);
      untrustedCertIssuer = getBooleanPreference(existingPrefs, ASSUME_UNTRUSTED_ISSUER_PREF, true);
    } else {
      acceptUntrustedCerts = true;
      untrustedCertIssuer = true;
    }

    // This is not entirely correct but this is not stored in the profile
    // so for now will always be set to false.
    loadNoFocusLib = false;
  }

  public static FirefoxProfile fromJson(String json) throws IOException {
    // We used to just pass in the entire string without quotes. If we see that, we're good.
    // Otherwise, parse the json.

    if (json.trim().startsWith("\"")) {
      json = new Json().toType(json, String.class);
    }

    return new FirefoxProfile(Zip.unzipToTempDir(json, "webdriver", "duplicated"));
  }

  private boolean getBooleanPreference(Preferences prefs, String key, boolean defaultValue) {
    Object value = prefs.getPreference(key);
    if (value == null) {
      return defaultValue;
    }

    if (value instanceof Boolean) {
      return (Boolean) value;
    }

    throw new WebDriverException("Expected boolean value is not a boolean. It is: " + value);
  }

  public String getStringPreference(String key, String defaultValue) {
    Object preference = additionalPrefs.getPreference(key);
    if (preference instanceof String) {
      return (String) preference;
    }
    return defaultValue;
  }

  public int getIntegerPreference(String key, int defaultValue) {
    Object preference = additionalPrefs.getPreference(key);
    if (preference instanceof Integer) {
      return (Integer) preference;
    }
    return defaultValue;
  }

  public boolean getBooleanPreference(String key, boolean defaultValue) {
    Object preference = additionalPrefs.getPreference(key);
    if (preference instanceof Boolean) {
      return (Boolean) preference;
    }
    return defaultValue;
  }

  private void verifyModel(File model) {
    if (model == null) {
      return;
    }

    if (!model.exists()) {
      throw new UnableToCreateProfileException(
          "Given model profile directory does not exist: " + model.getPath());
    }

    if (!model.isDirectory()) {
      throw new UnableToCreateProfileException(
          "Given model profile directory is not a directory: " + model.getAbsolutePath());
    }
  }

  public boolean containsWebDriverExtension() {
    return extensions.containsKey("webdriver");
  }

  public void addExtension(Class<?> loadResourcesUsing, String loadFrom) {
    // Is loadFrom a file?
    File file = new File(loadFrom);
    if (file.exists()) {
      addExtension(file);
      return;
    }

    addExtension(loadFrom, new ClasspathExtension(loadResourcesUsing, loadFrom));
  }

  /**
   * Attempt to add an extension to install into this instance.
   *
   * @param extensionToInstall File pointing to the extension
   */
  public void addExtension(File extensionToInstall) {
    addExtension(extensionToInstall.getName(), new FileExtension(extensionToInstall));
  }

  public void addExtension(String key, Extension extension) {
    String name = deriveExtensionName(key);
    extensions.put(name, extension);
  }

  private String deriveExtensionName(String originalName) {
    String[] pieces = originalName.replace('\\', '/').split("/");

    String name = pieces[pieces.length - 1];
    name = name.replaceAll("\\..*?$", "");
    return name;
  }

  public void setPreference(String key, Object value) {
    additionalPrefs.setPreference(key, value);
  }

  protected Preferences getAdditionalPreferences() {
    return additionalPrefs;
  }

  public void updateUserPrefs(File userPrefs) {
    Preferences prefs = new Preferences();

    // Allow users to override these settings
    prefs.setPreference("browser.startup.homepage", "about:blank");
    // The user must be able to override this setting (to 1) in order to
    // to change homepage on Firefox 3.0
    prefs.setPreference("browser.startup.page", 0);

    if (userPrefs.exists()) {
      prefs = new Preferences(userPrefs);
      if (!userPrefs.delete()) {
        throw new WebDriverException("Cannot delete existing user preferences");
      }
    }

    additionalPrefs.addTo(prefs);

    // Should we accept untrusted certificates or not?
    prefs.setPreference(ACCEPT_UNTRUSTED_CERTS_PREF, acceptUntrustedCerts);

    prefs.setPreference(ASSUME_UNTRUSTED_ISSUER_PREF, untrustedCertIssuer);

    // If the user sets the home page, we should also start up there
    Object homePage = prefs.getPreference("browser.startup.homepage");
    if (homePage instanceof String) {
      prefs.setPreference("startup.homepage_welcome_url", "");
    }

    if (!"about:blank".equals(prefs.getPreference("browser.startup.homepage"))) {
      prefs.setPreference("browser.startup.page", 1);
    }

    try (Writer writer =
        new OutputStreamWriter(new FileOutputStream(userPrefs), Charset.defaultCharset())) {
      prefs.writeTo(writer);
    } catch (IOException e) {
      throw new WebDriverException(e);
    }
  }

  protected void deleteLockFiles(File profileDir) {
    File macAndLinuxLockFile = new File(profileDir, ".parentlock");
    File windowsLockFile = new File(profileDir, "parent.lock");

    macAndLinuxLockFile.delete();
    windowsLockFile.delete();
  }

  public void deleteExtensionsCacheIfItExists(File profileDir) {
    File cacheFile = new File(profileDir, "extensions.cache");
    if (cacheFile.exists()) {
      cacheFile.delete();
    }
  }

  /**
   * Returns whether the no focus library should be loaded for Firefox profiles launched on Linux,
   * even if native events are disabled.
   *
   * @return Whether the no focus library should always be loaded for Firefox on Linux.
   */
  public boolean shouldLoadNoFocusLib() {
    return loadNoFocusLib;
  }

  /**
   * Sets whether the no focus library should always be loaded on Linux.
   *
   * @param loadNoFocusLib Whether to always load the no focus library.
   */
  public void setAlwaysLoadNoFocusLib(boolean loadNoFocusLib) {
    this.loadNoFocusLib = loadNoFocusLib;
  }

  /**
   * Sets whether Firefox should accept SSL certificates which have expired, signed by an unknown
   * authority or are generally untrusted. This is set to true by default.
   *
   * @param acceptUntrustedSsl Whether untrusted SSL certificates should be accepted.
   */
  public void setAcceptUntrustedCertificates(boolean acceptUntrustedSsl) {
    this.acceptUntrustedCerts = acceptUntrustedSsl;
  }

  /**
   * By default, when accepting untrusted SSL certificates, assume that these certificates will come
   * from an untrusted issuer or will be self signed. Due to limitation within Firefox, it is easy
   * to find out if the certificate has expired or does not match the host it was served for, but
   * hard to find out if the issuer of the certificate is untrusted.
   *
   * <p>By default, it is assumed that the certificates were not be issued from a trusted CA.
   *
   * <p>If you are receive an "untrusted site" prompt on Firefox when using a certificate that was
   * issued by valid issuer, but has expired or is being served served for a different host (e.g.
   * production certificate served in a testing environment) set this to false.
   *
   * @param untrustedIssuer whether to assume untrusted issuer or not.
   */
  public void setAssumeUntrustedCertificateIssuer(boolean untrustedIssuer) {
    this.untrustedCertIssuer = untrustedIssuer;
  }

  public void clean(File profileDir) {
    TemporaryFilesystem.getDefaultTmpFS().deleteTempDir(profileDir);
  }

  String toJson() throws IOException {
    File file = layoutOnDisk();
    try {
      return Zip.zip(file);
    } finally {
      clean(file);
    }
  }

  public void cleanTemporaryModel() {
    clean(model);
  }

  /**
   * Call this to cause the current profile to be written to disk. The profile directory is
   * returned. Note that this profile directory is a temporary one and will be deleted when the JVM
   * exists (at the latest)
   *
   * <p>This method should be called immediately before starting to use the profile and should only
   * be called once per instance of the {@link org.openqa.selenium.firefox.FirefoxDriver}.
   *
   * @return The directory containing the profile.
   */
  public File layoutOnDisk() {
    try {
      File profileDir =
          TemporaryFilesystem.getDefaultTmpFS().createTempDir("anonymous", "webdriver-profile");
      File userPrefs = new File(profileDir, "user.js");

      copyModel(model, profileDir);
      installExtensions(profileDir);
      deleteLockFiles(profileDir);
      deleteExtensionsCacheIfItExists(profileDir);
      updateUserPrefs(userPrefs);
      return profileDir;
    } catch (IOException e) {
      throw new UnableToCreateProfileException(e);
    }
  }

  protected void copyModel(File sourceDir, File profileDir) throws IOException {
    if (sourceDir == null || !sourceDir.exists()) {
      return;
    }

    FileHandler.copy(sourceDir, profileDir);
  }

  protected void installExtensions(File parentDir) throws IOException {
    File extensionsDir = new File(parentDir, "extensions");

    for (Extension extension : extensions.values()) {
      extension.writeTo(extensionsDir);
    }
  }
}
