// 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 static org.openqa.selenium.firefox.FirefoxProfile.PORT_PREFERENCE;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.firefox.internal.ClasspathExtension;
import org.openqa.selenium.firefox.internal.Extension;
import org.openqa.selenium.firefox.internal.FileExtension;
import org.openqa.selenium.remote.service.DriverService;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.Optional;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class XpiDriverService extends DriverService {

  private final Lock lock = new ReentrantLock();

  private final int port;
  private final FirefoxBinary binary;
  private final FirefoxProfile profile;
  private File profileDir;

  private XpiDriverService(
      File executable,
      int port,
      ImmutableList<String> args,
      ImmutableMap<String, String> environment,
      FirefoxBinary binary,
      FirefoxProfile profile)
      throws IOException {
    super(executable, port, args, environment);

    Preconditions.checkState(port > 0, "Port must be set");

    this.port = port;
    this.binary = binary;
    this.profile = profile;

    String firefoxLogFile = System.getProperty(FirefoxDriver.SystemProperty.BROWSER_LOGFILE);

    if (firefoxLogFile !=  null) {
      if ("/dev/stdout".equals(firefoxLogFile)) {
        sendOutputTo(System.out);
      } else {
        sendOutputTo(new FileOutputStream(firefoxLogFile));
      }
    }
  }

  @Override
  protected URL getUrl(int port) throws IOException {
    return new URL("http", "localhost", port, "/hub");
  }

  @Override
  public void start() throws IOException {
    lock.lock();
    try {
      profile.setPreference(PORT_PREFERENCE, port);
      addWebDriverExtension(profile);
      profileDir = profile.layoutOnDisk();

      binary.setOutputWatcher(getOutputStream());

      binary.startProfile(profile, profileDir, "-foreground");

      waitUntilAvailable();
    } finally {
      lock.unlock();
    }
  }

  @Override
  public void stop() {
    lock.lock();
    try {
      binary.quit();
      profile.cleanTemporaryModel();
      profile.clean(profileDir);
    } finally {
      lock.unlock();
    }
  }

  private void addWebDriverExtension(FirefoxProfile profile) {
    if (profile.containsWebDriverExtension()) {
      return;
    }
    profile.addExtension("webdriver", loadCustomExtension().orElse(loadDefaultExtension()));
  }

  private Optional<Extension> loadCustomExtension() {
    String xpiProperty = System.getProperty(FirefoxDriver.SystemProperty.DRIVER_XPI_PROPERTY);
    if (xpiProperty != null) {
      File xpi = new File(xpiProperty);
      return Optional.of(new FileExtension(xpi));
    }
    return Optional.empty();
  }

  private static Extension loadDefaultExtension() {
    return new ClasspathExtension(
        FirefoxProfile.class,
        "/" + FirefoxProfile.class.getPackage().getName().replace(".", "/") + "/webdriver.xpi");
  }

  /**
   * Configures and returns a new {@link XpiDriverService} using the default configuration. In
   * this configuration, the service will use the firefox executable identified by the
   * {@link FirefoxDriver.SystemProperty#BROWSER_BINARY} system property on a free port.
   *
   * @return A new XpiDriverService using the default configuration.
   */
  public static XpiDriverService createDefaultService() {
    try {
      return new XpiDriverService.Builder().usingAnyFreePort().build();
    } catch (WebDriverException e) {
      throw new IllegalStateException(e.getMessage(), e.getCause());
    }
  }

  public static Builder builder() {
    return new Builder();
  }

  public static class Builder extends DriverService.Builder<XpiDriverService, XpiDriverService.Builder> {

    private FirefoxBinary binary = null;
    private FirefoxProfile profile = null;

    private Builder() {
      // Only available through the static factory method in the XpiDriverService
    }

    public Builder withBinary(FirefoxBinary binary) {
      this.binary = Preconditions.checkNotNull(binary);
      return this;
    }

    public Builder withProfile(FirefoxProfile profile) {
      this.profile = Preconditions.checkNotNull(profile);
      return this;
    }

    @Override
    protected File findDefaultExecutable() {
      if (binary == null) {
        return new FirefoxBinary().getFile();
      }
      return binary.getFile();
    }

    @Override
    protected ImmutableList<String> createArgs() {
      return ImmutableList.of("-foreground");
    }

    @Override
    protected XpiDriverService createDriverService(
        File exe,
        int port,
        ImmutableList<String> args,
        ImmutableMap<String, String> environment) {
      try {
        return new XpiDriverService(
            exe,
            port,
            args,
            environment,
            binary == null ? new FirefoxBinary() : binary,
            profile == null ? new FirefoxProfile() : profile);
      } catch (IOException e) {
        throw new WebDriverException(e);
      }
    }
  }
}

@@ -909,16 +909,69 @@
 FERENCE;
+%0Aimport static java.util.concurrent.TimeUnit.SECONDS;
 %0A%0Aimport
@@ -1322,32 +1322,75 @@
 .FileExtension;%0A
+import org.openqa.selenium.net.UrlChecker;%0A
 import org.openq
@@ -1509,24 +1509,63 @@
 OException;%0A
+import java.net.MalformedURLException;%0A
 import java.
@@ -2700,34 +2700,44 @@
 nt port) throws 
-IO
+MalformedURL
 Exception %7B%0A    
@@ -3159,32 +3159,32 @@
     %7D finally %7B%0A
-
       lock.unloc
@@ -3191,32 +3191,402 @@
 k();%0A    %7D%0A  %7D%0A%0A
+  @Override%0A  protected void waitUntilAvailable() throws MalformedURLException %7B%0A    try %7B%0A      URL status = new URL(getUrl(port).toString() + %22/status%22);%0A      new UrlChecker().waitUntilAvailable(45, SECONDS, status);%0A    %7D catch (UrlChecker.TimeoutException e) %7B%0A      throw new WebDriverException(%22Timed out waiting 45 seconds for Firefox to start.%22, e);%0A    %7D%0A  %7D%0A%0A
   @Override%0A  pu

Raise XpiDriverService connect timeout to 45s.