/*
    GNU GENERAL PUBLIC LICENSE
    Copyright (C) 2006 The Lobo Project

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public
    License as published by the Free Software Foundation; either
    verion 2 of the License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    General Public License for more details.

    You should have received a copy of the GNU General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

    Contact info: lobochief@users.sourceforge.net
 */
package org.lobobrowser.gui;

import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Window;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.JComponent;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

import org.eclipse.jdt.annotation.NonNull;
import org.lobobrowser.clientlet.ClientletRequest;
import org.lobobrowser.clientlet.ClientletResponse;
import org.lobobrowser.clientlet.ComponentContent;
import org.lobobrowser.main.ExtensionManager;
import org.lobobrowser.main.PlatformInit;
import org.lobobrowser.request.ClientletRequestHandler;
import org.lobobrowser.request.ClientletRequestImpl;
import org.lobobrowser.request.DomainValidation;
import org.lobobrowser.request.RequestEngine;
import org.lobobrowser.request.RequestHandler;
import org.lobobrowser.request.SilentUserAgentContextImpl;
import org.lobobrowser.security.GenericLocalPermission;
import org.lobobrowser.security.RequestManager;
import org.lobobrowser.ua.NavigationEntry;
import org.lobobrowser.ua.NavigationEvent;
import org.lobobrowser.ua.NavigationListener;
import org.lobobrowser.ua.NavigationVetoException;
import org.lobobrowser.ua.NavigatorFrame;
import org.lobobrowser.ua.NavigatorProgressEvent;
import org.lobobrowser.ua.NetworkRequest;
import org.lobobrowser.ua.ParameterInfo;
import org.lobobrowser.ua.RequestType;
import org.lobobrowser.ua.TargetType;
import org.lobobrowser.ua.UserAgentContext;
import org.lobobrowser.ua.UserAgentContext.Request;
import org.lobobrowser.util.ArrayUtilities;
import org.lobobrowser.util.Items;
import org.lobobrowser.util.SecurityUtil;
import org.lobobrowser.util.WrapperException;
import org.lobobrowser.util.gui.WrapperLayout;

/**
 * A browser frame panel. It may be used as any other Swing component. The
 * {@link #navigate(String)} method may be invoked to load content into the
 * browser frame.
 * <p>
 * Content types supported depend on available browser extensions.
 * <p>
 * It's recommended that <code>FramePanel</code>s be placed in windows that
 * extend {@link AbstractBrowserWindow} or implement {@link BrowserWindow}. Such
 * windows may receive navigation notifications via {@link WindowCallback}.
 * <p>
 * A frame panel with navigation controls and a status bar can be obtained with
 * {@link BrowserPanel}.
 *
 * @see PlatformInit#init(boolean, boolean)
 */
public class FramePanel extends JPanel implements NavigatorFrame {
  private static final long serialVersionUID = -8873769110035409639L;
  private static final Logger logger = Logger.getLogger(FramePanel.class.getName());
  private final String windowId;
  private final NavigationEngine navigationEngine = new NavigationEngine();
  private final FramePanel knownParentFrame;
  private final Collection<NavigationListener> navigationListeners = new ArrayList<>();
  private final Collection<ResponseListener> responseListeners = new ArrayList<>();
  private final Collection<ContentListener> contentListeners = new ArrayList<>();
  private final Object propertiesMonitor = new Object();

  private NavigatorFrame openerFrame;
  private Window topFrameWindow;

  /**
   * Constructs a FramePanel specifying a "window" ID.
   */
  public FramePanel(final String windowId) {
    this.knownParentFrame = null;
    this.windowId = windowId;
    this.setLayout(WrapperLayout.getInstance());
    this.setBackground(Color.WHITE);
    this.setOpaque(true);
  }

  /**
   * Constructs a FramePanel specifying a non-null parent frame. This
   * constructor is useful when navigation in the new frame must occur before
   * the frame is added to the GUI component hierarchy.
   */
  public FramePanel(final FramePanel parentFrame) {
    this.knownParentFrame = parentFrame;
    this.windowId = null;
    this.setLayout(WrapperLayout.getInstance());
    this.setBackground(Color.WHITE);
    this.setOpaque(true);
  }

  /**
   * Constructs a standalone <code>FramePanel</code> that can be added to any
   * Swing window or component. Note that the FramePanel should be part of a
   * Swing or AWT window before it becomes functional.
   */
  public FramePanel() {
    this((FramePanel) null);
  }

  public void setOpenerFrame(final NavigatorFrame opener) {
    this.openerFrame = opener;
  }

  /**
   * Causes an event to be fired. This method is for internal use.
   *
   * @param response
   *          A clientlet response.
   */
  public void informResponseProcessed(final ClientletResponse response) {
    this.dispatchResponseProcessed(new ResponseEvent(this, response));
  }

  /**
   * Adds a listener of navigation events.
   *
   * @param listener
   *          The listener.
   */
  public void addNavigationListener(final NavigationListener listener) {
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
      sm.checkPermission(org.lobobrowser.security.GenericLocalPermission.EXT_GENERIC);
    }
    synchronized (this) {
      this.navigationListeners.add(listener);
    }
  }

  /**
   * Removes a listener of navigation events previously added with
   * {@link #addNavigationListener(NavigationListener)}.
   *
   * @param listener
   *          The listener.
   */
  public void removeNavigationListener(final NavigationListener listener) {
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
      sm.checkPermission(org.lobobrowser.security.GenericLocalPermission.EXT_GENERIC);
    }
    synchronized (this) {
      this.navigationListeners.remove(listener);
    }
  }

  /**
   * Adds a listener of content events.
   *
   * @param listener
   *          The listener.
   * @see #getComponentContent()
   */
  public void addContentListener(final ContentListener listener) {
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
      sm.checkPermission(org.lobobrowser.security.GenericLocalPermission.EXT_GENERIC);
    }
    synchronized (this) {
      this.contentListeners.add(listener);
    }
  }

  /**
   * Removes a listener of content events previously added with
   * {@link #addNavigationListener(NavigationListener)}.
   *
   * @param listener
   *          The listener.
   */
  public void removeContentListener(final ContentListener listener) {
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
      sm.checkPermission(org.lobobrowser.security.GenericLocalPermission.EXT_GENERIC);
    }
    synchronized (this) {
      this.contentListeners.remove(listener);
    }
  }

  /**
   * Adds a listener of response events.
   *
   * @param listener
   *          The listener.
   */
  public void addResponseListener(final ResponseListener listener) {
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
      sm.checkPermission(org.lobobrowser.security.GenericLocalPermission.EXT_GENERIC);
    }
    synchronized (this) {
      this.responseListeners.add(listener);
    }
  }

  /**
   * Removes a listener of navigation events previously added with
   * {@link #addNavigationListener(NavigationListener)}.
   *
   * @param listener
   *          The listener.
   */
  public void removeResponseListener(final ResponseListener listener) {
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
      sm.checkPermission(org.lobobrowser.security.GenericLocalPermission.EXT_GENERIC);
    }
    synchronized (this) {
      this.responseListeners.remove(listener);
    }
  }

  private void dispatchBeforeNavigate(final NavigationEvent event) throws NavigationVetoException {
    try {
      AccessController.doPrivileged(new PrivilegedAction<Object>() {
        // Reason: Dispatching an event to extensions requires permission to,
        // among other things, setting the context class loader.
        public Object run() {
          try {
            ExtensionManager.getInstance().dispatchBeforeNavigate(event);
          } catch (final NavigationVetoException nve) {
            throw new WrapperException(nve);
          }
          ArrayUtilities.forEachSynched(navigationListeners, this, (listener) -> {
            try {
              listener.beforeNavigate(event);
            } catch (final NavigationVetoException nve) {
              throw new WrapperException(nve);
            }
          });
          return null;
        }
      });
    } catch (final WrapperException we) {
      throw (NavigationVetoException) we.getCause();
    }
  }

  private void dispatchBeforeLocalNavigate(final NavigationEvent event) throws NavigationVetoException {
    try {
      AccessController.doPrivileged(new PrivilegedAction<Object>() {
        // Reason: Dispatching an event to extensions requires permission to,
        // among other things, setting the context class loader.
        public Object run() {
          try {
            ExtensionManager.getInstance().dispatchBeforeLocalNavigate(event);
          } catch (final NavigationVetoException nve) {
            throw new WrapperException(nve);
          }
          ArrayUtilities.forEachSynched(navigationListeners, this, (listener) -> {
            try {
              listener.beforeLocalNavigate(event);
            } catch (final NavigationVetoException nve) {
              throw new WrapperException(nve);
            }
          });
          return null;
        }
      });
    } catch (final WrapperException we) {
      throw (NavigationVetoException) we.getCause();
    }
  }

  private void dispatchBeforeWindowOpen(final NavigationEvent event) throws NavigationVetoException {
    try {
      AccessController.doPrivileged(new PrivilegedAction<Object>() {
        // Reason: Dispatching an event to extensions requires permission to,
        // among other things, setting the context class loader.
        public Object run() {
          try {
            ExtensionManager.getInstance().dispatchBeforeWindowOpen(event);
          } catch (final NavigationVetoException nve) {
            throw new WrapperException(nve);
          }
          ArrayUtilities.forEachSynched(navigationListeners, this, (listener) -> {
            try {
              listener.beforeWindowOpen(event);
            } catch (final NavigationVetoException nve) {
              throw new WrapperException(nve);
            }
          });
          return null;
        }
      });
    } catch (final WrapperException we) {
      throw (NavigationVetoException) we.getCause();
    }
  }

  private void dispatchContentSet(final ContentEvent event) {
    ArrayUtilities.forEachSynched(contentListeners, this, (listener) -> {
      listener.contentSet(event);
    });
  }

  private void dispatchResponseProcessed(final ResponseEvent event) {
    ArrayUtilities.forEachSynched(responseListeners, this, (listener) -> {
      listener.responseProcessed(event);
    });
  }

  /**
   * Gets a {@link WindowCallback} instance that is used to dispatch information
   * during local navigation. The FramePanel tries to find an implementor of the
   * interface among its ancestor components. Unless overridden, this
   * implementation of <code>getWindowCallback</code> only looks for instances
   * of the {@link BrowserWindow} interface.
   */
  protected WindowCallback getWindowCallback() {
    final FramePanel kpf = this.knownParentFrame;
    if (kpf != null) {
      return kpf.getWindowCallback();
    }
    Container parent = this.getParent();
    while ((parent != null) && !(parent instanceof BrowserWindow)) {
      parent = parent.getParent();
    }
    if (parent == null) {
      return null;
    }
    return ((BrowserWindow) parent).getWindowCallback();
  }

  /**
   * Gets the parent frame. This is <code>null<code> for the top-most frame
   * in a window or when the FramePanel is detached.
   */
  public NavigatorFrame getParentFrame() {
    // TODO: Security?
    final NavigatorFrame kpf = this.knownParentFrame;
    if (kpf != null) {
      return kpf;
    }
    Container parent = this.getParent();
    while ((parent != null) && !(parent instanceof NavigatorFrame)) {
      parent = parent.getParent();
    }
    return (NavigatorFrame) parent;
  }

  /**
   * Gets the top-most frame in a window. It may return the current frame if its
   * parent is <code>null</code>.
   */
  public NavigatorFrame getTopFrame() {
    NavigatorFrame current = this;
    for (;;) {
      final NavigatorFrame ancestor = current.getParentFrame();
      if (ancestor == null) {
        return current;
      }
      current = ancestor;
    }
  }

  /**
   * Implements {@link NavigatorFrame#getComponent()}.
   */
  public Component getComponent() {
    return this;
  }

  @Override
  public void paint(final Graphics g) {
    // Unless done this way, duplicate
    // painting occurs for nested frames.
    this.paintComponent(g);
    this.paintBorder(g);
    this.paintChildren(g);
  }

  /**
   * Gets an array of navigation entries that came before the current one.
   */
  public List<NavigationEntry> getBackNavigationEntries() {
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
      sm.checkPermission(org.lobobrowser.security.GenericLocalPermission.EXT_GENERIC);
    }
    synchronized (this) {
      return this.navigationEngine.getBackNavigationEntries();
    }
  }

  /**
   * Gets an array of navigation entries that would be visited with consecutive
   * <code>forward</code> calls.
   */
  public List<NavigationEntry> getForwardNavigationEntries() {
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
      sm.checkPermission(org.lobobrowser.security.GenericLocalPermission.EXT_GENERIC);
    }
    synchronized (this) {
      return this.navigationEngine.getForwardNavigationEntries();
    }
  }

  /**
   * Determines if the current navigation entry has associated source code.
   */
  public boolean hasSource() {
    // TODO: Security?
    final ComponentContent content = this.content;
    return (content != null) && (content.getSourceCode() != null);
  }

  /**
   * Determines whether there's a selection with content to be copied in this
   * frame.
   */
  public boolean canCopy() {
    // TODO: Security?
    final ComponentContent content = this.content;
    return content == null ? false : content.canCopy();
  }

  /**
   * Copies the selection, if any, to the clipboard. Whether this method is
   * supported depends on content being rendered.
   */
  public boolean copy() {
    // TODO: Security?
    final ComponentContent content = this.content;
    return content == null ? false : content.copy();
  }

  /*
  public final void replaceContent(final Component component) {
    // TODO: Security?
    this.replaceContent(null, new SimpleComponentContent(component));
  }
  */

  /**
   * Replaces the content of the frame. This method can be safely called outside
   * the GUI dispatch thread.
   */
  public void replaceContent(final ClientletResponse response, final ComponentContent content) {
    // Method probably invoked outside GUI thread.
    if (SwingUtilities.isEventDispatchThread()) {
      this.replaceContentImpl(response, content);
    } else {
      // Security note: Need to pass security context of caller
      // into invokeLater task.
      final AccessControlContext context = AccessController.getContext();
      SwingUtilities.invokeLater(() -> {
        final PrivilegedAction<Object> action = () -> {
          FramePanel.this.replaceContentImpl(response, content);
          return null;
        };
        AccessController.doPrivileged(action, context);
      });
    }
  }

  @Override
  protected void addImpl(final Component comp, final Object constraints, final int index) {
    // Check security. Content downloaded off the web should
    // not be allowed to replace frame content at will.
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
      sm.checkPermission(GenericLocalPermission.EXT_GENERIC);
    }
    super.addImpl(comp, constraints, index);
  }

  @Override
  public void remove(final Component comp) {
    // Check security. Content downloaded off the web should
    // not be allowed to replace frame content at will.
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
      sm.checkPermission(GenericLocalPermission.EXT_GENERIC);
    }
    super.remove(comp);
  }

  @Override
  public void remove(final int index) {
    // Check security. Content downloaded off the web should
    // not be allowed to replace frame content at will.
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
      sm.checkPermission(GenericLocalPermission.EXT_GENERIC);
    }
    super.remove(index);
  }

  @Override
  public void removeAll() {
    // Check security. Content downloaded off the web should
    // not be allowed to replace frame content at will.
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
      sm.checkPermission(GenericLocalPermission.EXT_GENERIC);
    }
    super.removeAll();
  }

  protected void replaceContentImpl(final ClientletResponse response, final ComponentContent content) {
    // Security note: Currently expected to be private.
    // Always called in GUI thread.
    // removeAll and add will invalidate.
    final ComponentContent oldContent = this.content;
    this.removeAll();
    if (oldContent != null) {
      oldContent.removeNotify();
    }
    if (content != null) {
      final Component component = content.getComponent();
      if (component == null) {
        throw new java.lang.IllegalStateException("Component from " + content + " is null: " + response.getResponseURL() + ".");
      }
      this.add(component);
    }
    // Call to validate will lay out children.
    this.validate();
    this.repaint();

    // Set this at the end, after removal and addition of components has
    // succeded.
    this.content = content;
    if (content != null) {
      content.addNotify();
      this.updateContentProperties(content);
    }

    if (response != null) {
      final String title = content == null ? null : content.getTitle();
      final String description = content == null ? null : content.getDescription();
      final NavigationEntry navigationEntry = NavigationEntry.fromResponse(this, response, title, description);
      if (response.isNewNavigationAction()) {
        synchronized (this) {
          this.navigationEngine.addNavigationEntry(navigationEntry);
        }
      }

      if (content != null) {
        if (PlatformInit.getInstance().debugOn) {
          System.out.println("Navigation over: " + response.getResponseURL());
        }
        content.navigatedNotify();
      }

      final WindowCallback wc = this.getWindowCallback();
      if (wc != null) {
        // It's important that the handleDocumentRendering method be called
        // right after navigationEngine is updated.
        // TODO: Why?
        wc.handleDocumentRendering(this, response, content);
      }
    } else {
      // Notify so that lazy layouting algorithm can know that layouting is not blocked
      if (content != null) {
        content.navigatedNotify();
      }
    }
    this.dispatchContentSet(new ContentEvent(this, content, response));
  }

  /**
   * Clears current content. This method should be invoked in the GUI thread.
   */
  public void clear() {
    this.removeAll();
    this.content = null;
  }

  private Window getWindow() {
    // TODO: Security? Getting parent security?
    final FramePanel kpf = this.knownParentFrame;
    if (kpf != null) {
      return kpf.getWindow();
    }
    Container parent = this.getParent();
    if (parent instanceof FramePanel) {
      return ((FramePanel) parent).getWindow();
    }
    while ((parent != null) && !(parent instanceof Window)) {
      parent = parent.getParent();
    }
    return (Window) parent;
  }

  /**
   * Closes the window this frame belongs to.
   */
  public void closeWindow() {
    // TODO: Security?
    final Window window = this.getWindow();
    if (window != null) {
      window.dispose();
    }
  }

  /**
   * Opens a confirmation dialog.
   */
  public boolean confirm(final String message) {
    return AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
      // Reason: We don't want an "Applet Window" message and
      // it's no big deal to allow it here.
      public Boolean run() {
        return JOptionPane.showConfirmDialog(FramePanel.this, message) == JOptionPane.YES_OPTION;
      }
    });
  }

  /**
   * Implements {@link NavigatorFrame#invokeLater(Runnable)}.
   */
  public void invokeLater(final Runnable runnable) {
    SwingUtilities.invokeLater(runnable);
  }

  public final void navigate(final String urlOrPath) throws java.net.MalformedURLException {
    final URL url = DomainValidation.guessURL(urlOrPath);
    this.navigate(url, "GET", null, TargetType.SELF, RequestType.PROGRAMMATIC);
  }

  public final void navigate(final String urlOrPath, final RequestType requestType) throws java.net.MalformedURLException {
    final URL url = DomainValidation.guessURL(urlOrPath);
    this.navigate(url, "GET", null, TargetType.SELF, requestType);
  }

  public void navigate(final @NonNull URL url, final String method, final ParameterInfo paramInfo, final TargetType type,
      final RequestType requestType) {
    this.navigate(url, method, paramInfo, type, requestType, this);
  }

  public void navigate(final @NonNull URL url, final String method, final ParameterInfo paramInfo, final TargetType type,
      final RequestType requestType, final NavigatorFrame originatingFrame) {
    final NavigationEvent event = new NavigationEvent(this, url, method, paramInfo, type, requestType, originatingFrame);
    this.navigate(event);
  }

  private void navigate(final NavigationEvent event) {
    try {
      this.dispatchBeforeNavigate(event);
    } catch (final NavigationVetoException nve) {
      if (logger.isLoggable(Level.INFO)) {
        logger.info("navigateLocal(): Navigation was vetoed: " + nve.getMessage());
      }
      return;
    }
    final TargetType type = event.getTargetType();
    final URL url = event.getURL();
    final String method = event.getMethod();
    final ParameterInfo paramInfo = event.getParamInfo();
    final RequestType requestType = event.getRequestType();
    switch (type) {
    case PARENT:
      final NavigatorFrame parent = this.getParentFrame();
      if (parent != null) {
        parent.navigate(url, method, paramInfo, TargetType.SELF, requestType, this);
      } else {
        this.navigateLocal(event);
      }
      break;
    case TOP:
      final NavigatorFrame top = this.getTopFrame();
      if (top == this) {
        this.navigateLocal(event);
      } else {
        top.navigate(url, method, paramInfo, TargetType.SELF, requestType, this);
      }
      break;
    case SELF:
      this.navigateLocal(event);
      break;
    case BLANK:
      this.open(url, method, paramInfo);
      break;
    }
  }

  private void navigateToHistoryEntry(final @NonNull URL url) {
    this.navigateLocal(url, "GET", RequestType.HISTORY, this);
  }

  protected boolean isOKToAddReferrer(final RequestType requestType) {
    return (requestType == RequestType.CLICK) || (requestType == RequestType.PROGRAMMATIC)
        || (requestType == RequestType.PROGRAMMATIC_FROM_CLICK) || (requestType == RequestType.OPEN_WINDOW)
        || (requestType == RequestType.OPEN_WINDOW_FROM_CLICK) || (requestType == RequestType.FORM);
  }

  private void navigateLocal(final @NonNull URL url, final String method, final RequestType requestType, final FramePanel originatingFrame) {
    final NavigationEvent event = new NavigationEvent(this, url, method, requestType, originatingFrame);
    this.navigateLocal(event);
  }

  private void navigateLocal(final NavigationEvent event) {
    requestManager.reset(event.getURL());
    try {
      this.dispatchBeforeLocalNavigate(event);
    } catch (final NavigationVetoException nve) {
      if (logger.isLoggable(Level.INFO)) {
        logger.info("navigateLocal(): Navigation was vetoed: " + nve.getMessage());
      }
      return;
    }
    String referrer = null;
    final RequestType requestType = event.getRequestType();
    final URL url = event.getURL();
    final String method = event.getMethod();
    final ParameterInfo paramInfo = event.getParamInfo();
    if (this.isOKToAddReferrer(requestType)) {
      // TODO: When child frame does a _top navigate, referrer
      // should apparently be from child.
      final NavigationEntry entry = this.getCurrentNavigationEntry();
      if (entry != null) {
        referrer = entry.getUrl().toExternalForm();
      }
    }
    final ClientletRequest request = new ClientletRequestImpl(false, url, method, paramInfo, null, referrer, null, requestType);
    final UserAgentContext uaContext = new SilentUserAgentContextImpl(this);
    final RequestHandler handler = new ClientletRequestHandler(request, this.getWindowCallback(), this, uaContext);
    SecurityUtil.doPrivileged(() -> {
      // Justification: While requests by untrusted code are generally only
      // allowed on certain hosts, navigation is an exception.
        RequestEngine.getInstance().scheduleRequest(handler);
        return null;
      });
  }

  /**
   * Opens a window and renders the URL or path provided.
   *
   * @return The top frame of the new window.
   */
  public final NavigatorFrame open(final String urlOrPath) throws MalformedURLException {
    final URL url = DomainValidation.guessURL(urlOrPath);
    return this.open(url, (Properties) null);
  }

  /**
   * Opens a window and renders the URL provided.
   *
   * @param url
   *          The URL of the document.
   * @param windowProperties
   *          A Properties object that follows JavaScript Window.open()
   *          conventions.
   * @return The top frame of the new window.
   */
  public final NavigatorFrame open(final @NonNull URL url, final Properties windowProperties) {
    return this.open(url, null, windowProperties);
  }

  /**
   * Opens a window and renders the URL provided.
   *
   * @param url
   *          The URL of the document.
   * @param windowId
   *          A unique ID for the window.
   * @param windowProperties
   *          A Properties object that follows JavaScript Window.open()
   *          conventions.
   * @return The top frame of the new window.
   */
  public final NavigatorFrame open(final @NonNull URL url, final String windowId, final Properties windowProperties) {
    return this.open(url, "GET", null, windowId, windowProperties);
  }

  /**
   * Opens a window and renders the URL provided.
   *
   * @param url
   *          The URL of the document.
   * @return The top frame of the new window.
   */
  public final NavigatorFrame open(final @NonNull URL url) {
    return this.open(url, (Properties) null);
  }

  /**
   * Opens a window and renders the URL provided. This method is called to
   * request that a new window is opened. For example, this method will be
   * invoked on JavaScript Window.open() calls. Override to be informed of such
   * calls.
   *
   * @return The top frame of the new window. The method may return
   *         <code>null</code> if navigation was vetoed by a listener.
   */
  public NavigatorFrame open(final @NonNull URL url, final String method, final ParameterInfo pinfo, final String windowId,
      final Properties windowProperties) {
    final NavigationEvent event = new NavigationEvent(this, url, method, pinfo, TargetType.BLANK, RequestType.OPEN_WINDOW, this);
    try {
      this.dispatchBeforeWindowOpen(event);
    } catch (final NavigationVetoException nve) {
      if (logger.isLoggable(Level.INFO)) {
        logger.info("navigateLocal(): Navigation was vetoed: " + nve.getMessage());
      }
      return null;
    }
    return FramePanel.openWindow(this, url, windowId, windowProperties, method, pinfo);
  }

  /**
   * Opens a window and renders the URL provided.
   *
   * @param url
   *          The URL of the document.
   * @param method
   *          The request method.
   * @param pinfo
   *          Any additional parameter data.
   * @return The top frame of the new window.
   */
  public final NavigatorFrame open(final @NonNull URL url, final String method, final ParameterInfo pinfo) {
    return this.open(url, method, pinfo, null, null);
  }

  /**
   * Static method for opening a window.
   *
   * @return The top frame in the window that was opened.
   */
  public static NavigatorFrame openWindow(final FramePanel opener, final @NonNull URL url, final String windowId, final Properties windowProperties,
      final String method, final ParameterInfo pinfo) {
    final ClientletRequest request = new ClientletRequestImpl(true, url, method, pinfo, RequestType.OPEN_WINDOW);
    final NavigatorWindowImpl wcontext = AccessController.doPrivileged(new PrivilegedAction<NavigatorWindowImpl>() {
      // Reason: Window creation can require special permissions at various
      // levels, e.g. ExtensionManager access and os.version check in Swing.
      public NavigatorWindowImpl run() {
        return new NavigatorWindowImpl(opener, windowId, windowProperties);
      }
    });

    final FramePanel newFrame = wcontext.getFramePanel();
    newFrame.requestManager.reset(url);

    final UserAgentContext uaContext = new SilentUserAgentContextImpl(newFrame);
    final ClientletRequestHandler handler = new ClientletRequestHandler(request, wcontext, newFrame, uaContext);
    SwingUtilities.invokeLater(() -> wcontext.resetAsNavigator(handler.getContextWindowProperties()));

    SecurityUtil.doPrivileged(() -> {
      // Justification: While requests by untrusted code are generally only allowed on certain hosts,
      // navigation is an exception.
        RequestEngine.getInstance().scheduleRequest(handler);
        return null;
      });

    return newFrame;
  }

  /**
   * Opens a message prompt dialog.
   */
  public String prompt(final String message, final String inputDefault) {
    return AccessController.doPrivileged(new PrivilegedAction<String>() {
      // Reason: We don't want an "Applet Window" message and
      // it's no big deal to allow it here.
      public String run() {
        return JOptionPane.showInputDialog(FramePanel.this, message, inputDefault);
      }
    });
  }

  /**
   * Sends the window to the back (blur).
   */
  public void windowToBack() {
    final Window window = this.getWindow();
    if (window != null) {
      window.toBack();
    }
  }

  /**
   * Sends the window to the front and grabs focus for the frame.
   */
  public void windowToFront() {
    final Window window = this.getWindow();
    if (window != null) {
      window.toFront();
    }
    this.grabFocus();
  }

  /**
   * Opens an alert dialog.
   */
  public void alert(final String message) {
    AccessController.doPrivileged(new PrivilegedAction<Object>() {
      // Reason: We don't want an "Applet Window" message and
      // it's no big deal to allow it here.
      public Object run() {
        JOptionPane.showMessageDialog(FramePanel.this, message);
        return null;
      }
    });
  }

  /**
   * Navigates to the given entry without adding the entry to frame history.
   * This is the mechanism that should be used to "go back" to an entry already
   * visited.
   */
  public boolean goTo(final NavigationEntry entry) {
    if (!"GET".equals(entry.getMethod())) {
      throw new IllegalArgumentException("Method only accepts entries with GET method.");
    }
    this.navigateToHistoryEntry(entry.getUrl());
    synchronized (this) {
      return this.navigationEngine.moveTo(entry);
    }
  }

  /**
   * Navigates back.
   */
  public boolean back() {
    return this.moveNavigation(-1);
  }

  /**
   * Navigates forward.
   */
  public boolean forward() {
    return this.moveNavigation(+1);
  }

  private boolean moveNavigation(final int offset) {
    if ((offset == 0) || (offset > 1) || (offset < -1)) {
      throw new IllegalArgumentException("offset: only +1 or -1 are allowed");
    }
    synchronized (this) {
      NavigationEntry newEntry;
      for (;;) {
        newEntry = this.navigationEngine.move(offset);
        if (newEntry == null) {
          return false;
        }
        if (!"GET".equals(newEntry.getMethod())) {
          // back() and forward() only supported for GET.
          continue;
        }
        break;
      }
      this.navigateToHistoryEntry(newEntry.getUrl());
      return true;
    }
  }

  /**
   * Determines whether the frame can navigate forward.
   */
  public boolean canForward() {
    synchronized (this) {
      return this.navigationEngine.hasNextWithGET();
    }
  }

  /**
   * Determines whether the frame can navigate back.
   */
  public boolean canBack() {
    synchronized (this) {
      return this.navigationEngine.hasPrevWithGET();
    }
  }

  /**
   * Reloads the current document.
   */
  public void reload() {
    NavigationEntry entry;
    synchronized (this) {
      entry = this.navigationEngine.getCurrentEntry();
    }
    if (entry != null) {
      final String method = entry.getMethod();
      if (!"GET".equals(method)) {
        final String lineBreak = System.getProperty("line.separator");
        this.alert("Reloading a document not obtained with the GET " + lineBreak + "method is disallowed for security reasons." + lineBreak
            + "The request method of the current page is " + method + ".");

      } else {
        this.navigateLocal(entry.getUrl(), entry.getMethod(), RequestType.SOFT_RELOAD, this);
      }
    }
  }

  /**
   * Determines whether the current document can be reloaded.
   */
  public boolean canReload() {
    NavigationEntry entry;
    synchronized (this) {
      entry = this.navigationEngine.getCurrentEntry();
    }
    // Check for request method or not?
    return entry != null;
  }

  /**
   * Creates a frame that is expected to be used as a child of the current one.
   */
  public NavigatorFrame createFrame() {
    return FramePanelFactorySource.getInstance().getActiveFactory().createFramePanel(this);
  }

  /**
   * Gets the default window status.
   */
  public String getDefaultStatus() {
    final WindowCallback wc = this.getWindowCallback();
    if (wc != null) {
      return wc.getDefaultStatus();
    } else {
      return null;
    }
  }

  public Object getItem(final String name) {
    return Items.getItem(this, name);
  }

  /**
   * Gets the frame that opened the current frame, if any.
   */
  public NavigatorFrame getOpenerFrame() {
    return this.openerFrame;
  }

  /**
   * Gets the current window status.
   */
  public String getStatus() {
    final WindowCallback wc = this.getWindowCallback();
    if (wc != null) {
      return wc.getStatus();
    } else {
      return null;
    }
  }

  /**
   * Gets the window ID if this is the top frame in a window.
   */
  public String getWindowId() {
    return this.windowId;
  }

  /**
   * Determines if the window is closed.
   */
  public boolean isWindowClosed() {
    final Window window = this.getWindow();
    if (window != null) {
      return !window.isDisplayable();
    }
    return true;
  }

  /**
   * Sets the default window status.
   */
  public void setDefaultStatus(final String value) {
    final WindowCallback wc = this.getWindowCallback();
    if (wc != null) {
      wc.setDefaultStatus(this, value);
    }
  }

  public void setItem(final String name, final Object value) {
    Items.setItem(this, name, value);
  }

  /**
   * Sets the window status.
   */
  public void setStatus(final String status) {
    final WindowCallback wc = this.getWindowCallback();
    if (wc != null) {
      wc.setStatus(this, status);
    }
  }

  @Override
  public Dimension getPreferredSize() {
    if (this.isPreferredSizeSet()) {
      return super.getPreferredSize();
    } else {
      return new Dimension(600, 400);
    }
  }

  @Override
  public Dimension getMinimumSize() {
    return new Dimension(1, 1);
  }

  @Override
  public Dimension getMaximumSize() {
    return new Dimension(Short.MAX_VALUE, Short.MAX_VALUE);
  }

  private ComponentContent content;

  /**
   * Gets the component content currently set in the frame.
   */
  public ComponentContent getComponentContent() {
    // TODO: Security?
    return this.content;
  }

  public String getSourceCode() {
    final ComponentContent content = this.content;
    return content == null ? null : content.getSourceCode();
  }

  public final void navigate(final @NonNull URL url) {
    this.navigate(url, RequestType.PROGRAMMATIC);
  }

  public final void navigate(final @NonNull URL url, final RequestType requestType) {
    this.navigate(url, "GET", null, TargetType.SELF, requestType);
  }

  /**
   * Gets the current navigation entry.
   */
  public NavigationEntry getCurrentNavigationEntry() {
    synchronized (this) {
      return this.navigationEngine.getCurrentEntry();
    }
  }

  public NavigatorProgressEvent getProgressEvent() {
    return this.progressEvent;
  }

  private NavigatorProgressEvent progressEvent;

  public void setProgressEvent(final NavigatorProgressEvent event) {
    this.progressEvent = event;
    if (event != null) {
      final WindowCallback wc = this.getWindowCallback();
      if (wc != null) {
        wc.updateProgress(event);
      }
    }
  }

  public NetworkRequest createNetworkRequest() {
    final UserAgentContext uaContext = new SilentUserAgentContextImpl(this);
    return new org.lobobrowser.context.NetworkRequestImpl(uaContext);
  }

  @Override
  public String toString() {
    return "FramePanel[windowId=" + windowId + ",hashCode=" + this.hashCode() + ",parent=" + this.getParent() + "]";
  }

  public void resizeWindowBy(final int byWidth, final int byHeight) {
    final Window window = this.getWindow();
    if (logger.isLoggable(Level.INFO)) {
      logger.info("resizeWindowBy(): byWidth=" + byWidth + ",byHeight=" + byHeight + "; window=" + window);
    }
    if (window != null) {
      window.setSize(window.getWidth() + byWidth, window.getHeight() + byHeight);
    }
  }

  public void resizeWindowTo(final int width, final int height) {
    final Window window = this.getWindow();
    if (logger.isLoggable(Level.INFO)) {
      logger.info("resizeWindowTo(): width=" + width + ",height=" + height + "; window=" + window);
    }
    if (window != null) {
      window.setSize(width, height);
    }
  }

  public Window getTopFrameWindow() {
    return topFrameWindow;
  }

  public void setTopFrameWindow(final Window topFrameWindow) {
    this.topFrameWindow = topFrameWindow;
  }

  /**
   * Gets an object that is used to represent the current frame content. For
   * example, if the frame is currently showing HTML, this method will probably
   * return an instance of <code>org.w3c.dom.html2.HTMLDocument</code>.
   */
  public Object getContentObject() {
    final ComponentContent content = this.getComponentContent();
    return content == null ? null : content.getContentObject();
  }

  /**
   * Gets a mime type that goes with the object returned by
   * {@link FramePanel#getContentObject()}. This is not necessarily the same as
   * the mime type declared in the headers of the response that produced the
   * current content.
   */
  public String getCurrentMimeType() {
    final ComponentContent content = this.getComponentContent();
    return content == null ? null : content.getMimeType();
  }

  public void linkClicked(final @NonNull URL url, final TargetType targetType, final Object linkObject) {
    final NavigationEvent event = new NavigationEvent(this, url, targetType, RequestType.CLICK, linkObject, this);
    this.navigate(event);
  }

  public int getHistoryLength() {
    synchronized (this) {
      return this.navigationEngine.getLength();
    }
  }

  public Optional<NavigationEntry> getNextNavigationEntry() {
    synchronized (this) {
      final List<NavigationEntry> entries = this.navigationEngine.getForwardNavigationEntries();
      return entries.stream().findFirst();
    }
  }

  public Optional<NavigationEntry> getPreviousNavigationEntry() {
    synchronized (this) {
      final List<NavigationEntry> entries = this.navigationEngine.getBackNavigationEntries();
      return entries.stream().findFirst();
    }
  }

  public void moveInHistory(final int offset) {
    this.moveNavigation(offset);
  }

  public void navigateInHistory(final String absoluteURL) {
    NavigationEntry entry;
    synchronized (this) {
      entry = this.navigationEngine.findEntry(absoluteURL);
    }
    if (entry != null) {
      this.navigateToHistoryEntry(entry.getUrl());
    }
  }

  private Map<String, Object> contentProperties = null;

  public void setProperty(final String name, final Object value) {
    final ComponentContent content = this.getComponentContent();
    synchronized (this.propertiesMonitor) {
      if (content != null) {
        content.setProperty(name, value);
      }
      Map<String, Object> props = this.contentProperties;
      if (props == null) {
        props = new HashMap<>(5);
        this.contentProperties = props;
      }
      props.put(name, value);
    }
  }

  private void updateContentProperties(final ComponentContent content) {
    synchronized (this.propertiesMonitor) {
      final Map<String, Object> props = this.contentProperties;
      if (props != null) {
        final Iterator<Entry<String, Object>> i = props.entrySet().iterator();
        while (i.hasNext()) {
          final Entry<String, Object> entry = i.next();
          content.setProperty(entry.getKey(), entry.getValue());
        }
      }
    }
  }

  private final RequestManager requestManager = new RequestManager(this);

  public boolean isRequestPermitted(final Request request) {
    return requestManager.isRequestPermitted(request);
  }

  public void manageRequests(final Object initiator) {
    requestManager.manageRequests((JComponent) initiator);
  }

  public void allowAllFirstPartyRequests() {
    requestManager.allowAllFirstPartyRequests();
  }
}
