// Copyright (c) 2013 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

package org.cef;

import com.jetbrains.cef.JCefAppConfig;
import com.jetbrains.cef.JdkEx;
import com.jetbrains.cef.remote.browser.RemoteBrowser;
import com.jetbrains.cef.remote.browser.RemoteClient;
import org.cef.browser.*;
import org.cef.callback.*;
import org.cef.handler.*;
import org.cef.misc.BoolRef;
import org.cef.misc.CefLog;
import org.cef.misc.CefPrintSettings;
import org.cef.misc.CefRange;
import org.cef.network.CefRequest;
import org.cef.network.CefRequest.TransitionType;
import org.cef.security.CefSSLInfo;

import java.awt.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
 * Client that owns a browser and renderer.
 */
public class CefClient extends CefClientHandler
        implements CefContextMenuHandler, CefDialogHandler, CefDisplayHandler, CefDownloadHandler,
                   CefDragHandler, CefFocusHandler, CefPermissionHandler, CefJSDialogHandler, CefKeyboardHandler,
                   CefLifeSpanHandler, CefLoadHandler, CefPrintHandler, CefRenderHandler,
                   CefRequestHandler, CefWindowHandler {
    private static final boolean TRACE_LIFESPAN = Boolean.getBoolean("jcef.trace.cefclient.lifespan");
    // Delegate for remote implementation.
    private final RemoteClient remoteClient;

    // Fields for JNI implementation.
    private final ConcurrentHashMap<Integer, CefBrowser> browser_ = new ConcurrentHashMap<Integer, CefBrowser>();
    private CefContextMenuHandler contextMenuHandler_ = null;
    private CefDialogHandler dialogHandler_ = null;
    private CefDisplayHandler displayHandler_ = null;
    private CefDownloadHandler downloadHandler_ = null;
    private CefDragHandler dragHandler_ = null;
    private CefFocusHandler focusHandler_ = null;
    private CefPermissionHandler permissionHandler_ = null;
    private CefJSDialogHandler jsDialogHandler_ = null;
    private CefKeyboardHandler keyboardHandler_ = null;
    private final List<CefLifeSpanHandler> lifeSpanHandlers_ = new ArrayList<>();
    private CefLoadHandler loadHandler_ = null;
    private CefPrintHandler printHandler_ = null;
    private CefRequestHandler requestHandler_ = null;
    private boolean isDisposed_ = false;
    private Runnable onDisposed_ = null; // just for convenience (for tests debugging)
    private volatile CefBrowser focusedBrowser_ = null;
    // allow(by making propertyChangeListener protected) custom disposing of this listener for GTW-4830
    // TODO(kharitonov): make dispose() work w/o warnings and side effects for rem-dev server implementation
    protected final PropertyChangeListener propertyChangeListener = new PropertyChangeListener() {
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            if (focusedBrowser_ != null && !focusedBrowser_.isWindowless()) {
                Component browserUI = focusedBrowser_.getUIComponent();
                if (browserUI == null) return;
                Object oldUI = evt.getOldValue();
                if (isPartOf(oldUI, browserUI)) {
                    focusedBrowser_.setFocus(false);
                    focusedBrowser_ = null;
                }
            }
        }
    };

    /**
     * The CTOR is only accessible within this package.
     * Use CefApp.createClient() to create an instance of
     * this class.
     *
     * @see org.cef.CefApp#createClient()
     */
    protected CefClient() throws UnsatisfiedLinkError {
        super();
        remoteClient = null;
        KeyboardFocusManager.getCurrentKeyboardFocusManager().addPropertyChangeListener(propertyChangeListener);
        if (TRACE_LIFESPAN) CefLog.Debug("CefClient %s: created in-process instance", this);
    }

    protected CefClient(RemoteClient _remoteClient) {
        super();
        remoteClient = _remoteClient;
        if (TRACE_LIFESPAN) CefLog.Debug("CefClient %s: created remote client %s", this, remoteClient);
    }

    private boolean isPartOf(Object obj, Component browserUI) {
        if (obj == browserUI) return true;
        if (obj instanceof Container) {
            Component childs[] = ((Container) obj).getComponents();
            for (Component child : childs) {
                return isPartOf(child, browserUI);
            }
        }
        return false;
    }

    @Override
    public void dispose() {
        if (TRACE_LIFESPAN) CefLog.Debug("CefClient: dispose client %s [remote=%s]", this, remoteClient);
        isDisposed_ = true;
        if (remoteClient != null) {
            // NOTE: super.dispose() shouldn't be called here
            CefApp app = remoteClient.getServer().getCefApp();
            remoteClient.dispose();
            if (app != null) app.clientWasDisposed(this);
            if (onDisposed_ != null) onDisposed_.run();
        } else
            cleanupBrowser(-1);
    }

    public void setOnDisposeCallback(Runnable onDisposed) {
        this.onDisposed_ = onDisposed;
    }
    // CefClientHandler

    /**
     * @deprecated {@link #createBrowser(String, CefRendering, boolean)}
     */
    @Deprecated
    public CefBrowser createBrowser(
            String url, boolean isOffscreenRendered, boolean isTransparent) {
        return createBrowser(url, isOffscreenRendered, isTransparent, null);
    }


    /**
     * @deprecated {@link #createBrowser(String, CefRendering, boolean, CefRequestContext)}
     */
    @Deprecated
    public CefBrowser createBrowser(String url, boolean isOffscreenRendered, boolean isTransparent,
                                    CefRequestContext context) {
        return createBrowser(url, isOffscreenRendered ? CefRendering.OFFSCREEN : CefRendering.DEFAULT, isTransparent, context);
    }

    @Deprecated
    public CefBrowser createBrowser(String url, boolean isOffscreenRendered, boolean isTransparent,
                                    CefRequestContext context, CefBrowserSettings settings) {
        return createBrowser(url, isOffscreenRendered ? CefRendering.OFFSCREEN : CefRendering.DEFAULT, isTransparent, context, settings);
    }

    public CefBrowser createBrowser(String url, CefRendering rendering, boolean isTransparent) {
        return createBrowser(url, rendering, isTransparent, null);
    }

    public CefBrowser createBrowser(String url, CefRendering rendering, boolean isTransparent,
                                    CefRequestContext context) {
        if (isDisposed_)
            throw new IllegalStateException("Can't create browser. CefClient is disposed");
        if (remoteClient != null)
            return remoteClient.createBrowser(url, context, this, rendering, null);
        return CefBrowserFactory.create(this, url, rendering, isTransparent, context, null);
    }

    public CefBrowser createBrowser(String url, CefRendering rendering, boolean isTransparent,
                                    CefRequestContext context, CefBrowserSettings settings) {
        if (isDisposed_)
            throw new IllegalStateException("Can't create browser. CefClient is disposed");
        if (remoteClient != null)
            return remoteClient.createBrowser(url, context, this, rendering, settings);
        return CefBrowserFactory.create(
                this, url, rendering, isTransparent, context, settings);
    }

    public CefBrowser createBrowser(String url, Supplier<CefRendering> rendering, boolean isTransparent,
                                    CefRequestContext context, CefBrowserSettings settings) {
        if (isDisposed_)
            throw new IllegalStateException("Can't create browser. CefClient is disposed");
        if (remoteClient != null)
            return remoteClient.createBrowser(url, context, this, rendering, settings);
        return CefBrowserFactory.create(
                this, url, rendering.get(), isTransparent, context, settings);
    }

    @Override
    protected CefBrowser getBrowser(int identifier) {
        if (remoteClient != null)
            return remoteClient.getRemoteBrowser(identifier);
        return browser_.get(identifier);
    }

    @Override
    protected Object[] getAllBrowser() {
        if (remoteClient != null)
            return remoteClient.getAllBrowsers();
        return browser_.values().stream().filter(browser -> !browser.isClosing()).toArray();
    }

    @Override
    protected CefContextMenuHandler getContextMenuHandler() {
        if (remoteClient != null)
            return remoteClient.getContextMenuHandler();
        return this;
    }

    @Override
    protected CefDialogHandler getDialogHandler() {
        if (remoteClient != null)
            return remoteClient.getDialogHandler();
        return this;
    }

    @Override
    protected CefDisplayHandler getDisplayHandler() {
        if (remoteClient != null)
            return remoteClient.getDisplayHandler();
        return this;
    }

    @Override
    protected CefDownloadHandler getDownloadHandler() {
        if (remoteClient != null)
            return remoteClient.getDownloadHandler();
        return this;
    }

    @Override
    protected CefDragHandler getDragHandler() {
        if (remoteClient != null)
            return remoteClient.getDragHandler();
        return this;
    }

    @Override
    protected CefFocusHandler getFocusHandler() {
        if (remoteClient != null)
            return remoteClient.getFocusHandler();
        return this;
    }

    @Override
    protected CefPermissionHandler getPermissionHandler() {
        if (remoteClient != null)
            return remoteClient.getPermissionHandler();
        return this;
    }

    @Override
    protected CefJSDialogHandler getJSDialogHandler() {
        if (remoteClient != null)
            return remoteClient.getJSDialogHandler();
        return this;
    }

    @Override
    protected CefKeyboardHandler getKeyboardHandler() {
        if (remoteClient != null)
            return remoteClient.getKeyboardHandler();
        return this;
    }

    @Override
    protected CefLifeSpanHandler getLifeSpanHandler() {
        if (remoteClient != null) {
            CefLog.Error("CefClient.getLifeSpanHandler mustn't be called in remote mode.");
            return null;
        }
        return this;
    }

    @Override
    protected CefLoadHandler getLoadHandler() {
        if (remoteClient != null)
            return remoteClient.getLoadHandler();
        return this;
    }

    @Override
    protected CefPrintHandler getPrintHandler() {
        if (remoteClient != null)
            return remoteClient.getPrintHandler();
        return this;
    }

    @Override
    protected CefRenderHandler getRenderHandler() {
        if (remoteClient != null) {
            CefLog.Error("CefClient.getRenderHandler mustn't be called in remote mode.");
            return null;
        }
        return this;
    }

    @Override
    protected CefRequestHandler getRequestHandler() {
        if (remoteClient != null)
            return remoteClient.getRequestHandler();
        return this;
    }

    @Override
    protected CefWindowHandler getWindowHandler() {
        if (remoteClient != null)
            return null; // only OSR rendering in remote mode
        return this;
    }

    // CefContextMenuHandler

    public CefClient addContextMenuHandler(CefContextMenuHandler handler) {
        if (remoteClient != null)
            remoteClient.addContextMenuHandler(handler);
        else
            if (contextMenuHandler_ == null) contextMenuHandler_ = handler;
        return this;
    }

    public void removeContextMenuHandler() {
        if (remoteClient != null)
            remoteClient.removeContextMenuHandler();
        else
            contextMenuHandler_ = null;
    }

    @Override
    public void onBeforeContextMenu(
            CefBrowser browser, CefFrame frame, CefContextMenuParams params, CefMenuModel model) {
        if (remoteClient != null) CefLog.Error("onBeforeContextMenu mustn't be called in remote mode (it seems that user manually called this method).");
        if (contextMenuHandler_ != null && browser != null)
            contextMenuHandler_.onBeforeContextMenu(browser, frame, params, model);
    }

    @Override
    public boolean runContextMenu(CefBrowser browser, CefFrame frame, CefContextMenuParams params, CefMenuModel model, CefRunContextMenuCallback callback) {
        if (remoteClient != null) CefLog.Error("The implementation for out-of-process is to be provided");
        if (contextMenuHandler_ != null && browser != null) {
            return contextMenuHandler_.runContextMenu(browser, frame, params, model, callback);
        }
        return false;
    }

    @Override
    public boolean onContextMenuCommand(CefBrowser browser, CefFrame frame,
                                        CefContextMenuParams params, int commandId, int eventFlags) {
        if (remoteClient != null) CefLog.Error("onContextMenuCommand mustn't be called in remote mode (it seems that user manually called this method).");
        if (contextMenuHandler_ != null && browser != null)
            return contextMenuHandler_.onContextMenuCommand(
                    browser, frame, params, commandId, eventFlags);
        return false;
    }

    @Override
    public void onContextMenuDismissed(CefBrowser browser, CefFrame frame) {
        if (remoteClient != null) CefLog.Error("onContextMenuDismissed mustn't be called in remote mode (it seems that user manually called this method).");
        if (contextMenuHandler_ != null && browser != null)
            contextMenuHandler_.onContextMenuDismissed(browser, frame);
    }

    // CefDialogHandler

    public CefClient addDialogHandler(CefDialogHandler handler) {
        if (remoteClient != null)
            remoteClient.addDialogHandler(handler);
        else
            if (dialogHandler_ == null) dialogHandler_ = handler;
        return this;
    }

    public void removeDialogHandler() {
        if (remoteClient != null)
            remoteClient.removeDialogHandler();
        else
            dialogHandler_ = null;
    }

    @Override
    public boolean onFileDialog(CefBrowser browser, FileDialogMode mode, String title,
            String defaultFilePath, Vector<String> acceptFilters, Vector<String> acceptExtensions,
            Vector<String> acceptDescriptions, CefFileDialogCallback callback) {
        if (remoteClient != null) CefLog.Error("onFileDialog mustn't be called in remote mode (it seems that user manually called this method).");
        if (dialogHandler_ != null && browser != null) {
            return dialogHandler_.onFileDialog(browser, mode, title, defaultFilePath, acceptFilters,
                    acceptExtensions, acceptDescriptions, callback);
        }
        return false;
    }

    // CefDisplayHandler

    public CefClient addDisplayHandler(CefDisplayHandler handler) {
        if (remoteClient != null)
            remoteClient.addDisplayHandler(handler);
        else
            if (displayHandler_ == null) displayHandler_ = handler;
        return this;
    }

    public void removeDisplayHandler() {
        if (remoteClient != null)
            remoteClient.removeDisplayHandler();
        else
            displayHandler_ = null;
    }

    @Override
    public void onAddressChange(CefBrowser browser, CefFrame frame, String url) {
        if (remoteClient != null) CefLog.Error("onAddressChange mustn't be called in remote mode (it seems that user manually called this method).");
        if (displayHandler_ != null && browser != null)
            displayHandler_.onAddressChange(browser, frame, url);
    }

    @Override
    public void onTitleChange(CefBrowser browser, String title) {
        if (remoteClient != null) CefLog.Error("onTitleChange mustn't be called in remote mode (it seems that user manually called this method).");
        if (displayHandler_ != null && browser != null)
            displayHandler_.onTitleChange(browser, title);
    }

    @Override
    public void onFullscreenModeChange(CefBrowser browser, boolean fullscreen) {
        if (displayHandler_ != null && browser != null)
            displayHandler_.onFullscreenModeChange(browser, fullscreen);
    }

    @Override
    public boolean onTooltip(CefBrowser browser, String text) {
        if (remoteClient != null) CefLog.Error("onTooltip mustn't be called in remote mode (it seems that user manually called this method).");
        if (displayHandler_ != null && browser != null) {
            return displayHandler_.onTooltip(browser, text);
        }
        return false;
    }

    @Override
    public void onStatusMessage(CefBrowser browser, String value) {
        if (remoteClient != null) CefLog.Error("onStatusMessage mustn't be called in remote mode (it seems that user manually called this method).");
        if (displayHandler_ != null && browser != null) {
            displayHandler_.onStatusMessage(browser, value);
        }
    }

    @Override
    public boolean onConsoleMessage(CefBrowser browser, CefSettings.LogSeverity level,
                                    String message, String source, int line) {
        if (remoteClient != null) CefLog.Error("onConsoleMessage mustn't be called in remote mode (it seems that user manually called this method).");
        if (displayHandler_ != null && browser != null) {
            return displayHandler_.onConsoleMessage(browser, level, message, source, line);
        }
        return false;
    }

    @Override
    public boolean onCursorChange(CefBrowser browser, int cursorType) {
        if (remoteClient != null) CefLog.Error("onCursorChange mustn't be called in remote mode (it seems that user manually called this method).");
        if (browser == null) {
            return false;
        }

        if (displayHandler_ != null && displayHandler_.onCursorChange(browser, cursorType)) {
            return true;
        }

        CefRenderHandler realHandler = browser.getRenderHandler();
        if (realHandler != null) {
            return realHandler.onCursorChange(browser, cursorType);
        }

        return false;
    }

    // CefDownloadHandler

    public CefClient addDownloadHandler(CefDownloadHandler handler) {
        if (remoteClient != null)
            remoteClient.addDownloadHandler(handler);
        else
            if (downloadHandler_ == null) downloadHandler_ = handler;
        return this;
    }

    public void removeDownloadHandler() {
        if (remoteClient != null)
            remoteClient.removeDownloadHandler();
        else
            downloadHandler_ = null;
    }

    @Override
    public boolean onBeforeDownload(CefBrowser browser, CefDownloadItem downloadItem,
            String suggestedName, CefBeforeDownloadCallback callback) {
        if (remoteClient != null) CefLog.Error("onBeforeDownload mustn't be called in remote mode (it seems that user manually called this method).");
        if (downloadHandler_ != null && browser != null)
            return downloadHandler_.onBeforeDownload(
                    browser, downloadItem, suggestedName, callback);
        return false;
    }

    @Override
    public void onDownloadUpdated(
            CefBrowser browser, CefDownloadItem downloadItem, CefDownloadItemCallback callback) {
        if (remoteClient != null) CefLog.Error("onDownloadUpdated mustn't be called in remote mode (it seems that user manually called this method).");
        if (downloadHandler_ != null && browser != null)
            downloadHandler_.onDownloadUpdated(browser, downloadItem, callback);
    }

    // CefDragHandler

    public CefClient addDragHandler(CefDragHandler handler) {
        if (remoteClient != null)
            remoteClient.addDragHandler(handler);
        else
            if (dragHandler_ == null) dragHandler_ = handler;
        return this;
    }

    public void removeDragHandler() {
        if (remoteClient != null)
            remoteClient.removeDragHandler();
        else
            dragHandler_ = null;
    }

    @Override
    public boolean onDragEnter(CefBrowser browser, CefDragData dragData, int mask) {
        if (remoteClient != null) CefLog.Error("onDragEnter mustn't be called in remote mode (it seems that user manually called this method).");
        if (dragHandler_ != null && browser != null)
            return dragHandler_.onDragEnter(browser, dragData, mask);
        return false;
    }

    // CefFocusHandler

    public CefClient addFocusHandler(CefFocusHandler handler) {
        if (remoteClient != null)
            remoteClient.addFocusHandler(handler);
        else
            if (focusHandler_ == null) focusHandler_ = handler;
        return this;
    }

    public void removeFocusHandler() {
        if (remoteClient != null)
            remoteClient.removeFocusHandler();
        else
            focusHandler_ = null;
    }

    @Override
    public void onTakeFocus(CefBrowser browser, boolean next) {
        if (remoteClient != null) CefLog.Error("onTakeFocus mustn't be called in remote mode (it seems that user manually called this method).");
        if (browser == null) return;

        browser.setFocus(false);
        Component uiComponent = browser.getUIComponent();
        if (uiComponent == null) return;
        Container parent = uiComponent.getParent();
        if (parent != null) {
            FocusTraversalPolicy policy = null;
            while (parent != null) {
                policy = parent.getFocusTraversalPolicy();
                if (policy != null) break;
                parent = parent.getParent();
            }
            if (policy != null) {
                Component nextComp = next
                        ? policy.getComponentAfter(parent, uiComponent)
                        : policy.getComponentBefore(parent, uiComponent);
                if (nextComp == null) {
                    policy.getDefaultComponent(parent).requestFocus();
                } else {
                    nextComp.requestFocus();
                }
            }
        }
        focusedBrowser_ = null;
        if (focusHandler_ != null) focusHandler_.onTakeFocus(browser, next);
    }

    @Override
    public boolean onSetFocus(final CefBrowser browser, FocusSource source) {
        if (remoteClient != null) CefLog.Error("onSetFocus mustn't be called in remote mode (it seems that user manually called this method).");
        if (browser == null) return false;

        Boolean alreadyHandled = Boolean.FALSE;
        if (focusHandler_ != null) {
            Component uiComponent = browser.getUIComponent();
            if (uiComponent == null) return true;
            alreadyHandled = JdkEx.invokeOnEDTAndWait(() -> focusHandler_.onSetFocus(browser, source), Boolean.TRUE /*ignore focus*/, uiComponent);
        }
        return alreadyHandled;
    }

    @Override
    public void onGotFocus(CefBrowser browser) {
        if (remoteClient != null) CefLog.Error("onGotFocus mustn't be called in remote mode (it seems that user manually called this method).");
        if (browser == null) return;
        if (focusedBrowser_ == browser) return; // prevent recursive call (in OSR)

        focusedBrowser_ = browser;
        browser.setFocus(true);
        if (focusHandler_ != null) {
            Component uiComponent = browser.getUIComponent();
            if (uiComponent == null) return;
            JdkEx.invokeOnEDTAndWait(() -> focusHandler_.onGotFocus(browser), uiComponent);
        }
    }

    // CefPermissionHandler

    public CefClient addPermissionHandler(CefPermissionHandler handler) {
        if (remoteClient != null)
            remoteClient.addPermissionHandler(handler);
        else
            if (permissionHandler_ == null) permissionHandler_ = handler;
        return this;
    }

    public void removePermissionHandler() {
        if (remoteClient != null)
            remoteClient.removePermissionHandler();
        else
            permissionHandler_ = null;
    }

    @Override
    public boolean onRequestMediaAccessPermission(
            CefBrowser browser,
            CefFrame frame,
            String requesting_url,
            int requested_permissions,
            CefMediaAccessCallback callback) {
        if (remoteClient != null) CefLog.Error("onRequestMediaAccessPermission mustn't be called in remote mode (it seems that user manually called this method).");
        if (permissionHandler_ != null && browser != null)
            return permissionHandler_.onRequestMediaAccessPermission(browser, frame, requesting_url,
                    requested_permissions, callback);
        return false;
    }

    // CefJSDialogHandler

    public CefClient addJSDialogHandler(CefJSDialogHandler handler) {
        if (remoteClient != null)
            remoteClient.addJSDialogHandler(handler);
        else
            if (jsDialogHandler_ == null) jsDialogHandler_ = handler;
        return this;
    }

    public void removeJSDialogHandler() {
        if (remoteClient != null)
            remoteClient.removeJSDialogHandler();
        else
            jsDialogHandler_ = null;
    }

    @Override
    public boolean onJSDialog(CefBrowser browser, String origin_url, JSDialogType dialog_type,
                              String message_text, String default_prompt_text, CefJSDialogCallback callback,
                              BoolRef suppress_message) {
        if (remoteClient != null) CefLog.Error("onJSDialog mustn't be called in remote mode (it seems that user manually called this method).");
        if (jsDialogHandler_ != null && browser != null)
            return jsDialogHandler_.onJSDialog(browser, origin_url, dialog_type, message_text,
                    default_prompt_text, callback, suppress_message);
        return false;
    }

    @Override
    public boolean onBeforeUnloadDialog(CefBrowser browser, String message_text, boolean is_reload,
                                        CefJSDialogCallback callback) {
        if (remoteClient != null) CefLog.Error("onBeforeUnloadDialog mustn't be called in remote mode (it seems that user manually called this method).");
        if (jsDialogHandler_ != null && browser != null)
            return jsDialogHandler_.onBeforeUnloadDialog(
                    browser, message_text, is_reload, callback);
        return false;
    }

    @Override
    public void onResetDialogState(CefBrowser browser) {
        if (remoteClient != null) CefLog.Error("onResetDialogState mustn't be called in remote mode (it seems that user manually called this method).");
        if (jsDialogHandler_ != null && browser != null)
            jsDialogHandler_.onResetDialogState(browser);
    }

    @Override
    public void onDialogClosed(CefBrowser browser) {
        if (remoteClient != null) CefLog.Error("onDialogClosed mustn't be called in remote mode (it seems that user manually called this method).");
        if (jsDialogHandler_ != null && browser != null) jsDialogHandler_.onDialogClosed(browser);
    }

    // CefKeyboardHandler

    public CefClient addKeyboardHandler(CefKeyboardHandler handler) {
        if (remoteClient != null)
            remoteClient.addKeyboardHandler(handler);
        else
            if (keyboardHandler_ == null) keyboardHandler_ = handler;
        return this;
    }

    public void removeKeyboardHandler() {
        if (remoteClient != null)
            remoteClient.removeKeyboardHandler();
        else
            keyboardHandler_ = null;
    }

    @Override
    public boolean onPreKeyEvent(
            CefBrowser browser, CefKeyEvent event, BoolRef is_keyboard_shortcut) {
        if (remoteClient != null) CefLog.Error("onPreKeyEvent mustn't be called in remote mode (it seems that user manually called this method).");
        if (keyboardHandler_ != null && browser != null)
            return keyboardHandler_.onPreKeyEvent(browser, event, is_keyboard_shortcut);
        return false;
    }

    @Override
    public boolean onKeyEvent(CefBrowser browser, CefKeyEvent event) {
        if (remoteClient != null) CefLog.Error("onKeyEvent mustn't be called in remote mode (it seems that user manually called this method).");
        if (keyboardHandler_ != null && browser != null)
            return keyboardHandler_.onKeyEvent(browser, event);
        return false;
    }

    // CefLifeSpanHandler

    public CefClient addLifeSpanHandler(CefLifeSpanHandler handler) {
        if (remoteClient != null)
            remoteClient.addLifeSpanHandler(handler);
        else
            synchronized (lifeSpanHandlers_) {
                lifeSpanHandlers_.add(handler);
            }
        return this;
    }

    public void removeLifeSpanHandler() {
        if (remoteClient != null)
            remoteClient.removeAllLifeSpanHandlers();
        else
            synchronized (lifeSpanHandlers_) {
                lifeSpanHandlers_.clear();
            }
    }

    @Override
    public boolean onBeforePopup(
            CefBrowser browser, CefFrame frame, String target_url, String target_frame_name) {
        if (remoteClient != null) CefLog.Error("onBeforePopup mustn't be called in remote mode (it seems that user manually called this method).");
        if (isDisposed_) return true;
        if (browser == null)
            return false;
        synchronized (lifeSpanHandlers_) {
            boolean result = false;
            for (CefLifeSpanHandler lsh : lifeSpanHandlers_) {
                result |= lsh.onBeforePopup(browser, frame, target_url, target_frame_name);
            }
            return result;
        }
    }

    @Override
    public void onAfterCreated(CefBrowser browser) {
        if (remoteClient != null) CefLog.Error("onAfterCreated mustn't be called in remote mode (it seems that user manually called this method).");
        if (browser == null) return;
        if (TRACE_LIFESPAN) CefLog.Debug("CefClient: browser=%s: onAfterCreated", browser);
        boolean disposed = isDisposed_;

        if (disposed) CefLog.Info("Browser %s was created while CefClient was marked as disposed", browser);

        // keep browser reference
        Integer identifier = browser.getIdentifier();
        if (!disposed) {
            browser_.put(identifier, browser);
        }
        synchronized (lifeSpanHandlers_) {
            for (CefLifeSpanHandler lsh : lifeSpanHandlers_)
                lsh.onAfterCreated(browser);
        }
        if (disposed) {
            // Not sure, but it makes sense to close browser since it's not in browser_
            browser.close(true);
        }
    }

    @Override
    public void onAfterParentChanged(CefBrowser browser) {
        if (remoteClient != null) CefLog.Error("onAfterParentChanged mustn't be called in remote mode (it seems that user manually called this method).");
        if (browser == null) return;
        synchronized (lifeSpanHandlers_) {
            for (CefLifeSpanHandler lsh : lifeSpanHandlers_)
                lsh.onAfterParentChanged(browser);
        }
    }

    @Override
    public boolean doClose(CefBrowser browser) {
        if (remoteClient != null) CefLog.Error("doClose mustn't be called in remote mode (it seems that user manually called this method).");
        if (browser == null) return false;
        synchronized (lifeSpanHandlers_) {
            for (CefLifeSpanHandler lsh : lifeSpanHandlers_)
                lsh.doClose(browser);
        }
        return browser.doClose();
    }

    @Override
    public void onBeforeClose(CefBrowser browser) {
        if (remoteClient != null) CefLog.Error("onBeforeClose mustn't be called in remote mode (it seems that user manually called this method).");
        if (browser == null) return;
        if (TRACE_LIFESPAN) CefLog.Debug("CefClient: browser=%s: onBeforeClose", browser);
        synchronized (lifeSpanHandlers_) {
            for (CefLifeSpanHandler lsh : lifeSpanHandlers_)
                lsh.onBeforeClose(browser);
        }
        browser.onBeforeClose();

        // remove browser reference
        cleanupBrowser(browser.getIdentifier());
    }

    private void cleanupBrowser(int identifier) {
        if (identifier >= 0) {
            // Remove the specific browser that closed.
            browser_.remove(identifier);
        } else {
            assert isDisposed_;
            Collection<CefBrowser> browserList = new ArrayList<>(browser_.values());
            if (!browserList.isEmpty()) {
                if (TRACE_LIFESPAN) CefLog.Debug("CefClient: cleanup %d browsers", browserList.size());
                // Close all browsers.
                // Once any of browsers close, it will invoke #onBeforeClose and #cleanupBrowser
                for (CefBrowser browser : browserList) {
                    if (TRACE_LIFESPAN) CefLog.Debug("CefClient: close %s", browser);
                    browser.close(true);
                }
                return;
            }
        }
        if (!isDisposed_) return;
        if (!browser_.isEmpty()) return;

        KeyboardFocusManager.getCurrentKeyboardFocusManager().removePropertyChangeListener(
                propertyChangeListener);
        removeContextMenuHandler(this);
        removeDialogHandler(this);
        removeDisplayHandler(this);
        removeDownloadHandler(this);
        removeDragHandler(this);
        removeFocusHandler(this);
        removeJSDialogHandler(this);
        removeKeyboardHandler(this);
        removeLifeSpanHandler(this);
        removeLoadHandler(this);
        removePrintHandler(this);
        removeRenderHandler(this);
        removeRequestHandler(this);
        removeWindowHandler(this);
        super.dispose();

        CefApp app = CefApp.getInstanceIfAny();
        if (app != null) app.clientWasDisposed(this);
        if (onDisposed_ != null) onDisposed_.run();
    }

    // CefLoadHandler

    public CefClient addLoadHandler(CefLoadHandler handler) {
        if (remoteClient != null)
            remoteClient.addLoadHandler(handler);
        else
            if (loadHandler_ == null) loadHandler_ = handler;
        return this;
    }

    public void removeLoadHandler() {
        if (remoteClient != null)
            remoteClient.removeLoadHandler();
        else
            loadHandler_ = null;
    }

    @Override
    public void onLoadingStateChange(
            CefBrowser browser, boolean isLoading, boolean canGoBack, boolean canGoForward) {
        if (remoteClient != null) CefLog.Error("onLoadingStateChange mustn't be called in remote mode (it seems that user manually called this method).");
        if (loadHandler_ != null && browser != null)
            loadHandler_.onLoadingStateChange(browser, isLoading, canGoBack, canGoForward);
    }

    @Override
    public void onLoadStart(CefBrowser browser, CefFrame frame, TransitionType transitionType) {
        if (remoteClient != null) CefLog.Error("onLoadStart mustn't be called in remote mode (it seems that user manually called this method).");
        if (loadHandler_ != null && browser != null)
            loadHandler_.onLoadStart(browser, frame, transitionType);
    }

    @Override
    public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
        if (remoteClient != null) CefLog.Error("onLoadEnd mustn't be called in remote mode (it seems that user manually called this method).");
        if (loadHandler_ != null && browser != null)
            loadHandler_.onLoadEnd(browser, frame, httpStatusCode);
    }

    @Override
    public void onLoadError(CefBrowser browser, CefFrame frame, ErrorCode errorCode,
                            String errorText, String failedUrl) {
        if (remoteClient != null) CefLog.Error("onLoadError mustn't be called in remote mode (it seems that user manually called this method).");
        if (loadHandler_ != null && browser != null)
            loadHandler_.onLoadError(browser, frame, errorCode, errorText, failedUrl);
    }

    // CefPrintHandler

    public CefClient addPrintHandler(CefPrintHandler handler) {
        if (remoteClient != null)
            remoteClient.addPrintHandler(handler);
        else
            if (printHandler_ == null) printHandler_ = handler;
        return this;
    }

    public void removePrintHandler() {
        if (remoteClient != null)
            remoteClient.removePrintHandler();
        else
            printHandler_ = null;
    }

    @Override
    public void onPrintStart(CefBrowser browser) {
        if (remoteClient != null) CefLog.Error("onPrintStart mustn't be called in remote mode (it seems that user manually called this method).");
        if (printHandler_ != null && browser != null) printHandler_.onPrintStart(browser);
    }

    @Override
    public void onPrintSettings(
            CefBrowser browser, CefPrintSettings settings, boolean getDefaults) {
        if (remoteClient != null) CefLog.Error("onPrintSettings mustn't be called in remote mode (it seems that user manually called this method).");
        if (printHandler_ != null && browser != null)
            printHandler_.onPrintSettings(browser, settings, getDefaults);
    }

    @Override
    public boolean onPrintDialog(
            CefBrowser browser, boolean hasSelection, CefPrintDialogCallback callback) {
        if (remoteClient != null) CefLog.Error("onPrintDialog mustn't be called in remote mode (it seems that user manually called this method).");
        if (printHandler_ != null && browser != null)
            return printHandler_.onPrintDialog(browser, hasSelection, callback);
        return false;
    }

    @Override
    public boolean onPrintJob(CefBrowser browser, String documentName, String pdfFilePath,
                              CefPrintJobCallback callback) {
        if (remoteClient != null) CefLog.Error("onPrintJob mustn't be called in remote mode (it seems that user manually called this method).");
        if (printHandler_ != null && browser != null)
            return printHandler_.onPrintJob(browser, documentName, pdfFilePath, callback);
        return false;
    }

    @Override
    public void onPrintReset(CefBrowser browser) {
        if (remoteClient != null) CefLog.Error("onPrintReset mustn't be called in remote mode (it seems that user manually called this method).");
        if (printHandler_ != null && browser != null) printHandler_.onPrintReset(browser);
    }

    @Override
    public Dimension getPdfPaperSize(CefBrowser browser, int deviceUnitsPerInch) {
        if (remoteClient != null) CefLog.Error("getPdfPaperSize mustn't be called in remote mode (it seems that user manually called this method).");
        if (printHandler_ != null && browser != null)
            return printHandler_.getPdfPaperSize(browser, deviceUnitsPerInch);
        return null;
    }

    // CefMessageRouter

    @Override
    public synchronized void addMessageRouter(CefMessageRouter messageRouter) {
        if (remoteClient != null)
            remoteClient.addMessageRouter(messageRouter);
        else
            super.addMessageRouter(messageRouter);
    }

    @Override
    public synchronized void removeMessageRouter(CefMessageRouter messageRouter) {
        if (remoteClient != null)
            remoteClient.removeMessageRouter(messageRouter);
        else
            super.removeMessageRouter(messageRouter);
    }

    // CefRenderHandler

    @Override
    public Rectangle getViewRect(CefBrowser browser) {
        if (remoteClient != null) CefLog.Error("getViewRect mustn't be called in remote mode (it seems that user manually called this method).");
        // [tav] resize to 1x1 size to avoid crash in cef
        if (browser == null) return new Rectangle(0, 0, 1, 1);

        CefRenderHandler realHandler = browser.getRenderHandler();
        if (realHandler != null) {
            Rectangle rect = realHandler.getViewRect(browser);
            if (rect.width <= 0 || rect.height <= 0) {
                rect = new Rectangle(rect.x, rect.y, 1, 1);
            }
            return rect;
        }
        return new Rectangle(0, 0, 1, 1);
    }

    @Override
    public Point getScreenPoint(CefBrowser browser, Point viewPoint) {
        if (remoteClient != null) CefLog.Error("getScreenPoint mustn't be called in remote mode (it seems that user manually called this method).");
        if (browser == null) return new Point(0, 0);

        CefRenderHandler realHandler = browser.getRenderHandler();
        if (realHandler != null) return realHandler.getScreenPoint(browser, viewPoint);
        return new Point(0, 0);
    }

    @Override
    public double getDeviceScaleFactor(CefBrowser browser) {
        if (remoteClient != null) CefLog.Error("getDeviceScaleFactor mustn't be called in remote mode (it seems that user manually called this method).");
        CefRenderHandler realHandler = browser.getRenderHandler();
        if (realHandler != null) {
            return realHandler.getDeviceScaleFactor(browser);
        }
        return JCefAppConfig.getDeviceScaleFactor(browser.getUIComponent());
    }

    @Override
    public void onPopupShow(CefBrowser browser, boolean show) {
        if (remoteClient != null) CefLog.Error("onPopupShow mustn't be called in remote mode (it seems that user manually called this method).");
        if (browser == null) return;

        CefRenderHandler realHandler = browser.getRenderHandler();
        if (realHandler != null) realHandler.onPopupShow(browser, show);
    }

    @Override
    public void onPopupSize(CefBrowser browser, Rectangle size) {
        if (remoteClient != null) CefLog.Error("onPopupSize mustn't be called in remote mode (it seems that user manually called this method).");
        if (browser == null) return;

        CefRenderHandler realHandler = browser.getRenderHandler();
        if (realHandler != null) realHandler.onPopupSize(browser, size);
    }

    @Override
    public void onPaint(CefBrowser browser, boolean popup, Rectangle[] dirtyRects,
                        ByteBuffer buffer, int width, int height) {
        if (remoteClient != null) CefLog.Error("onPaint mustn't be called in remote mode (it seems that user manually called this method).");
        if (browser == null) return;

        CefRenderHandler realHandler = browser.getRenderHandler();
        if (realHandler != null)
            realHandler.onPaint(browser, popup, dirtyRects, buffer, width, height);
    }

    @Override
    public void onAcceleratedPaint(CefBrowser browser, boolean popup, Rectangle[] dirtyRects, CefAcceleratedPaintInfo info) {
        if (remoteClient != null) CefLog.Error("onAcceleratedPaint mustn't be called in remote mode (it seems that user manually called this method).");
        if (browser == null) return;
        CefRenderHandler realHandler = browser.getRenderHandler();
        if (realHandler != null) {
            realHandler.onAcceleratedPaint(browser, popup, dirtyRects, info);
        }
    }

    @Override
    public void addOnPaintListener(Consumer<CefPaintEvent> listener) {}

    @Override
    public void setOnPaintListener(Consumer<CefPaintEvent> listener) {}

    @Override
    public void removeOnPaintListener(Consumer<CefPaintEvent> listener) {}

    @Override
    public boolean startDragging(CefBrowser browser, CefDragData dragData, int mask, int x, int y) {
        if (remoteClient != null) CefLog.Error("startDragging mustn't be called in remote mode (it seems that user manually called this method).");
        if (browser == null) return false;

        CefRenderHandler realHandler = browser.getRenderHandler();
        if (realHandler != null) return realHandler.startDragging(browser, dragData, mask, x, y);
        return false;
    }

    @Override
    public void updateDragCursor(CefBrowser browser, int operation) {
        if (remoteClient != null) CefLog.Error("updateDragCursor mustn't be called in remote mode (it seems that user manually called this method).");
        if (browser == null) return;

        CefRenderHandler realHandler = browser.getRenderHandler();
        if (realHandler != null) realHandler.updateDragCursor(browser, operation);
    }

    @Override
    public void OnImeCompositionRangeChanged(CefBrowser browser, CefRange selectionRange, Rectangle[] characterBounds) {
        if (remoteClient != null) CefLog.Error("OnImeCompositionRangeChanged mustn't be called in remote mode (it seems that user manually called this method).");
        if (browser == null) return;
        CefRenderHandler realHandler = browser.getRenderHandler();
        if (realHandler != null) realHandler.OnImeCompositionRangeChanged(browser, selectionRange, characterBounds);
    }

    @Override
    public void OnTextSelectionChanged(CefBrowser browser, String selectedText, CefRange selectionRange) {
        if (remoteClient != null) CefLog.Error("OnTextSelectionChanged mustn't be called in remote mode (it seems that user manually called this method).");
        if (browser == null) return;
        CefRenderHandler realHandler = browser.getRenderHandler();
        if (realHandler != null) realHandler.OnTextSelectionChanged(browser, selectedText, selectionRange);
    }

    @Override
    public boolean getScreenInfo(CefBrowser browser, CefScreenInfo screenInfo) {
        if (remoteClient != null) CefLog.Error("getScreenInfo mustn't be called in remote mode (it seems that user manually called this method).");
        if (browser == null) return false;

        CefRenderHandler realHandler = browser.getRenderHandler();
        if (realHandler != null) return realHandler.getScreenInfo(browser, screenInfo);
        return false;
    }

    // CefRequestHandler

    public CefClient addRequestHandler(CefRequestHandler handler) {
        if (remoteClient != null)
            remoteClient.addRequestHandler(handler);
        else
            if (requestHandler_ == null) requestHandler_ = handler;
        return this;
    }

    public void removeRequestHandler() {
        if (remoteClient != null)
            remoteClient.removeRequestHandler();
        else
            requestHandler_ = null;
    }

    @Override
    public boolean onBeforeBrowse(CefBrowser browser, CefFrame frame, CefRequest request,
                                  boolean user_gesture, boolean is_redirect) {
        if (remoteClient != null) CefLog.Error("onBeforeBrowse mustn't be called in remote mode (it seems that user manually called this method).");
        if (requestHandler_ != null && browser != null)
            return requestHandler_.onBeforeBrowse(
                    browser, frame, request, user_gesture, is_redirect);
        return false;
    }

    @Override
    public boolean onOpenURLFromTab(
            CefBrowser browser, CefFrame frame, String target_url, boolean user_gesture) {
        if (remoteClient != null) CefLog.Error("onOpenURLFromTab mustn't be called in remote mode (it seems that user manually called this method).");
        if (isDisposed_) return true;
        if (requestHandler_ != null && browser != null)
            return requestHandler_.onOpenURLFromTab(browser, frame, target_url, user_gesture);
        return false;
    }

    @Override
    public CefResourceRequestHandler getResourceRequestHandler(CefBrowser browser, CefFrame frame,
                                                               CefRequest request, boolean isNavigation, boolean isDownload, String requestInitiator,
                                                               BoolRef disableDefaultHandling) {
        if (remoteClient != null) CefLog.Error("getResourceRequestHandler mustn't be called in remote mode (it seems that user manually called this method).");
        if (requestHandler_ != null && browser != null) {
            return requestHandler_.getResourceRequestHandler(browser, frame, request, isNavigation,
                    isDownload, requestInitiator, disableDefaultHandling);
        }
        return null;
    }

    @Override
    public boolean getAuthCredentials(CefBrowser browser, String origin_url, boolean isProxy,
                                      String host, int port, String realm, String scheme, CefAuthCallback callback) {
        if (remoteClient != null) CefLog.Error("getAuthCredentials mustn't be called in remote mode (it seems that user manually called this method).");
        if (requestHandler_ != null && browser != null)
            return requestHandler_.getAuthCredentials(
                    browser, origin_url, isProxy, host, port, realm, scheme, callback);
        return false;
    }

    @Override
    public boolean onCertificateError(
            CefBrowser browser, ErrorCode cert_error, String request_url, CefSSLInfo sslInfo, CefCallback callback) {
        if (remoteClient != null) {
            CefLog.Error("onCertificateError mustn't be called in remote mode (it seems that user manually called this method).");
            // NOTE: next invocation us added just for IDEA tests.
            CefRequestHandler handler = remoteClient.getRequestHandler();
            if (handler != null)
                return handler.onCertificateError(browser, cert_error, request_url, sslInfo, callback);
        }
        if (requestHandler_ != null)
            return requestHandler_.onCertificateError(browser, cert_error, request_url, sslInfo, callback);
        return false;
    }

    @Override
    public void onRenderProcessTerminated(
            CefBrowser browser, TerminationStatus status, int error_code, String error_string) {
        if (remoteClient != null) CefLog.Error("onRenderProcessTerminated mustn't be called in remote mode (it seems that user manually called this method).");
        if (requestHandler_ != null)
            requestHandler_.onRenderProcessTerminated(browser, status, error_code, error_string);
    }

    // CefWindowHandler

    @Override
    public Rectangle getRect(CefBrowser browser) {
        if (remoteClient != null) CefLog.Error("getRect mustn't be called in remote mode (it seems that user manually called this method).");
        if (browser == null) return new Rectangle(0, 0, 0, 0);

        CefWindowHandler realHandler = browser.getWindowHandler();
        if (realHandler != null) return realHandler.getRect(browser);
        return new Rectangle(0, 0, 0, 0);
    }

    @Override
    public void onMouseEvent(
            CefBrowser browser, int event, int screenX, int screenY, int modifier, int button) {
        if (remoteClient != null) CefLog.Error("onMouseEvent mustn't be called in remote mode (it seems that user manually called this method).");
        if (browser == null) return;

        CefWindowHandler realHandler = browser.getWindowHandler();
        if (realHandler != null)
            realHandler.onMouseEvent(browser, event, screenX, screenY, modifier, button);
    }

    public String getInfo() {
        StringBuilder sb = new StringBuilder(toString());
        if (browser_.isEmpty())
            sb.append(" [0 active browsers]");
        else {
            sb.append(" | ");
            for (CefBrowser b : browser_.values()) {
                sb.append(b);
                sb.append(", ");
            }
        }
        return sb.toString();
    }

    public static boolean isNativeBrowserCreationStarted(CefBrowser browser) {
        if (browser instanceof RemoteBrowser)
            return ((RemoteBrowser)browser).isNativeBrowserCreationStarted();
        // Fallback to old logic (incorrect in general, since creation is always started before native ref obtained)
        if (browser instanceof CefNativeAdapter)
            return ((CefNativeAdapter)browser).getNativeRef("CefBrowser") != 0;

        CefLog.Error("Unsupported CefBrowser: %s", browser);
        return false;
    }

    public static boolean isNativeBrowserCreated(CefBrowser cefBrowser) {
        if (cefBrowser instanceof RemoteBrowser)
            return ((RemoteBrowser)cefBrowser).isNativeBrowserCreated();
        if (cefBrowser instanceof CefNativeAdapter)
            return ((CefNativeAdapter)cefBrowser).getNativeRef("CefBrowser") != 0;
        return false;
    }
}
