package com.jetbrains.cef.remote;

import com.jetbrains.cef.remote.callback.*;
import com.jetbrains.cef.remote.browser.RemoteBrowser;
import com.jetbrains.cef.remote.browser.RemoteDevToolsMessageObserver;
import com.jetbrains.cef.remote.browser.RemoteFrame;
import com.jetbrains.cef.remote.menu.RemoteMenuModel;
import com.jetbrains.cef.remote.network.*;
import com.jetbrains.cef.remote.router.RemoteMessageRouterHandler;
import com.jetbrains.cef.remote.router.RemoteQueryCallback;
import com.jetbrains.cef.remote.thrift_codegen.*;
import com.jetbrains.cef.remote.thrift_codegen.MenuItem;
import com.jetbrains.cef.remote.thrift_codegen.Point;
import com.jetbrains.cef.remote.thrift_codegen.Rect;
import com.jetbrains.cef.remote.thrift_codegen.ScreenInfo;
import com.jetbrains.cef.remote.thrift.TException;
import org.cef.CefSettings;
import org.cef.callback.*;
import org.cef.handler.*;
import org.cef.misc.*;
import org.cef.network.CefCookie;
import org.cef.network.CefRequest;
import org.cef.network.CefURLRequest;
import org.cef.security.CefSSLInfo;

import java.awt.*;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.List;

//
// Service for rpc from native to java
//
public class ClientHandlersImpl implements ClientHandlers.Iface {
    private static final boolean TRACE_REMOTE_FIND_BID = Utils.getBoolean("TRACE_REMOTE_FIND_BID");
    private Runnable myOnContextInitialized;
    private final RpcContext myRpc;

    public ClientHandlersImpl(RpcContext rpcContext) {
        myRpc = rpcContext;
    }

    public void setOnContextInitialized(Runnable onContextInitialized) {
        myOnContextInitialized = onContextInitialized;
    }

    private RemoteBrowser getRemoteBrowser(int bid) {
        RemoteBrowser browser = myRpc.server.bid2Browser.get(bid);
        if (browser == null) {
            if (TRACE_REMOTE_FIND_BID) CefLog.Debug("Can't find remote browser with bid=%d.", bid);
            return null;
        }
        return browser;
    }

    @Override
    public void log(String msg) {
        CefLog.Debug("received message from CefServer: " + msg);
    }

    @Override
    public String echo(String msg) {
        return msg;
    }

    //
    // CefApp
    //

    @Override
    public void AppHandler_OnContextInitialized() {
        // Called on the server process UI thread immediately after the CEF context
        // has been initialized.
        CefLog.Debug("AppHandler_OnContextInitialized: ");
        if (myOnContextInitialized != null)
            myOnContextInitialized.run();
    }

    //
    // CefRenderHandler
    //

    private static final Rect INVALID_RECT = new Rect(0,0,-1,-1);
    private static final Point INVALID_POINT = new Point(Integer.MIN_VALUE, Integer.MIN_VALUE);
    private static final ScreenInfo INVALID_SCREENINFO = new ScreenInfo(-1, -1, -1, false, new Rect(), new Rect());

    // NOTE: assume getRenderHandler() != null always

    @Override
    public Rect RenderHandler_GetViewRect(int bid) throws TException {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return INVALID_RECT;
        CefRenderHandler rh = browser.getRenderHandler();
        if (rh == null) return INVALID_RECT;
        Rectangle rect = rh.getViewRect(browser);
        return new Rect(rect.x, rect.y, rect.width, rect.height);
    }

    @Override
    public ScreenInfo RenderHandler_GetScreenInfo(int bid) throws TException {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return INVALID_SCREENINFO;
        CefRenderHandler rh = browser.getRenderHandler();
        if (rh == null) return INVALID_SCREENINFO;
        CefScreenInfo csi = new CefScreenInfo();
        boolean success = rh.getScreenInfo(browser, csi);
        return success ?
             new ScreenInfo(
                csi.device_scale_factor,
                csi.depth,
                csi.depth_per_component,
                csi.is_monochrome,
                new Rect(csi.x, csi.y, csi.width, csi.height),
                new Rect(csi.available_x, csi.available_y, csi.available_width, csi.available_height))
             : INVALID_SCREENINFO;
    }

    @Override
    public Point RenderHandler_GetScreenPoint(int bid, int viewX, int viewY) throws TException {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return INVALID_POINT;
        CefRenderHandler rh = browser.getRenderHandler();
        if (rh == null) return INVALID_POINT;
        java.awt.Point res = rh.getScreenPoint(browser, new java.awt.Point(viewX, viewY));
        return new Point(res.x, res.y);
    }

    @Override
    public void RenderHandler_OnPaint(int bid, boolean popup, int dirtyRectsCount, String sharedMemName, long sharedMemHandle, int width, int height) {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return;
        CefRenderHandler rh = browser.getRenderHandler();
        if (rh == null) return;
        ((CefNativeRenderHandler)rh).onPaintWithSharedMem(browser, popup, dirtyRectsCount, sharedMemName, sharedMemHandle, width, height);
    }

    @Override
    public void RenderHandler_OnPopupShow(int bid, boolean show) throws TException {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return;
        CefRenderHandler rh = browser.getRenderHandler();
        if (rh == null) return;
        rh.onPopupShow(browser, show);
    }

    @Override
    public void RenderHandler_OnPopupSize(int bid, Rect rect) throws TException {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return;
        CefRenderHandler rh = browser.getRenderHandler();
        if (rh == null) return;
        rh.onPopupSize(browser, new Rectangle(rect.x, rect.y, rect.w, rect.h));
    }

    @Override
    public void RenderHandler_OnTextSelectionChanged(int bid, String selectedText, Range selectionRange) throws TException {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return;
        CefRenderHandler rh = browser.getRenderHandler();
        if (rh == null) return;
        rh.OnTextSelectionChanged(browser, selectedText, new CefRange((int) selectionRange.from, (int) selectionRange.to));
    }

    @Override
    public void RenderHandler_OnImeCompositionRangeChanged(int bid, Range selectionRange, List<Rect> charactersBounds) throws TException {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return;
        CefRenderHandler rh = browser.getRenderHandler();
        if (rh == null) return;
        Rectangle[] rects = charactersBounds
                .stream()
                .map(rect -> new Rectangle(rect.x, rect.y, rect.w, rect.h))
                .toArray(Rectangle[]::new);
        rh.OnImeCompositionRangeChanged(browser, new CefRange((int) selectionRange.from, (int) selectionRange.to), rects);
    }

    //
    // CefLifeSpanHandler
    //

    @Override
    public boolean LifeSpanHandler_OnBeforePopup(int bid, RObject frame, String url, String frameName, boolean gesture) {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return false;

        RemoteFrame rframe = new RemoteFrame(myRpc, frame);
        return browser.getOwner().getLifeSpanHandler().handleBool(lsh-> lsh.onBeforePopup(browser, rframe, url, frameName));
    }

    @Override
    public void LifeSpanHandler_OnAfterCreated(int bid, int nativeBrowserIdentifier) {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return;

        browser.getOwner().onAfterCreated(browser, nativeBrowserIdentifier);
    }

    @Override
    public void LifeSpanHandler_OnBeforeClose(int bid) {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return;

        browser.getOwner().onBeforeClosed(browser);
    }

    /**
     * Called when a browser has received a request to close.
     *
     * If no OS window exists (window rendering disabled)
     * returning false will cause the browser object to be destroyed immediately. Return true if the
     * browser is parented to another window and that other window needs to receive close
     * notification via some non-standard technique.
     *
     * @param bid The browser generating the event.
     * @return False to send an OS close notification to the browser window's top-level owner.
     */
    @Override
    public boolean LifeSpanHandler_DoClose(int bid) {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser != null)
            browser.getOwner().getLifeSpanHandler().handle(lsh->lsh.doClose(browser));
        return false;
    }

    //
    // CefLoadHandler
    //
    @Override
    public void LoadHandler_OnLoadingStateChange(int bid, boolean isLoading, boolean canGoBack, boolean canGoForward) {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return;
        CefLoadHandler loadHandler = browser.getOwner().getLoadHandler();
        if (loadHandler == null) return;

        loadHandler.onLoadingStateChange(browser, isLoading, canGoBack, canGoForward);
    }

    private static CefRequest.TransitionType getTransitionType(int transitionTypeNative) {
        final int TT_SOURCE_MASK = 0xFF;
        final int TT_QUALIFIER_MASK = 0xFFFFFF00;
        CefRequest.TransitionType result = null;
        for (CefRequest.TransitionType tt: CefRequest.TransitionType.values()) {
            if ((tt.getValue() & TT_SOURCE_MASK) == (transitionTypeNative & TT_SOURCE_MASK)) {
                result = tt;
                break;
            }
        }

        if (result != null)
            result.addQualifiers(transitionTypeNative & TT_QUALIFIER_MASK);

        return result;
    }

    @Override
    public void LoadHandler_OnLoadStart(int bid, RObject frame, int transition_type) {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return;
        CefLoadHandler loadHandler = browser.getOwner().getLoadHandler();
        if (loadHandler == null) return;

        RemoteFrame rframe = new RemoteFrame(myRpc, frame);
        loadHandler.onLoadStart(browser, rframe, getTransitionType(transition_type));
    }

    @Override
    public void LoadHandler_OnLoadEnd(int bid, RObject frame, int httpStatusCode) {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return;
        CefLoadHandler loadHandler = browser.getOwner().getLoadHandler();
        if (loadHandler == null) return;

        RemoteFrame rframe = new RemoteFrame(myRpc, frame);
        loadHandler.onLoadEnd(browser, rframe, httpStatusCode);
    }

    @Override
    public void LoadHandler_OnLoadError(int bid, RObject frame, int errorCode, String errorText, String failedUrl) {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return;
        CefLoadHandler loadHandler = browser.getOwner().getLoadHandler();
        if (loadHandler == null) return;

        RemoteFrame rframe = new RemoteFrame(myRpc, frame);
        loadHandler.onLoadError(browser, rframe, CefLoadHandler.ErrorCode.findByCode(errorCode), errorText, failedUrl);
    }

    //
    // CefDisplayHandler
    //

    @Override
    public void DisplayHandler_OnAddressChange(int bid, RObject frame, String url) {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return;
        CefDisplayHandler dh = browser.getOwner().getDisplayHandler();
        if (dh == null) return;

        RemoteFrame rframe = new RemoteFrame(myRpc, frame);
        dh.onAddressChange(browser, rframe, url);
    }

    @Override
    public void DisplayHandler_OnTitleChange(int bid, String title) {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return;
        CefDisplayHandler dh = browser.getOwner().getDisplayHandler();
        if (dh == null) return;

        dh.onTitleChange(browser, title);
    }

    @Override
    public boolean DisplayHandler_OnTooltip(int bid, String text) {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return false;
        CefDisplayHandler dh = browser.getOwner().getDisplayHandler();
        if (dh == null) return false;

        return dh.onTooltip(browser, text);
    }

    @Override
    public void DisplayHandler_OnStatusMessage(int bid, String value) {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return;
        CefDisplayHandler dh = browser.getOwner().getDisplayHandler();
        if (dh == null) return;

        dh.onStatusMessage(browser, value);
    }

    @Override
    public boolean DisplayHandler_OnConsoleMessage(int bid, String level, String message, String source, int line) {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return false;
        CefDisplayHandler dh = browser.getOwner().getDisplayHandler();
        if (dh == null) return false;

        CefSettings.LogSeverity logSeverity = CefSettings.LogSeverity.LOGSEVERITY_DEFAULT;
        if ("verbose".equals(level))
            logSeverity = CefSettings.LogSeverity.LOGSEVERITY_VERBOSE;
        else if ("info".equals(level))
            logSeverity = CefSettings.LogSeverity.LOGSEVERITY_INFO;
        else if ("warning".equals(level))
            logSeverity = CefSettings.LogSeverity.LOGSEVERITY_WARNING;
        else if ("error".equals(level))
            logSeverity = CefSettings.LogSeverity.LOGSEVERITY_ERROR;
        else if ("fatal".equals(level))
            logSeverity = CefSettings.LogSeverity.LOGSEVERITY_FATAL;
        else if ("disable".equals(level))
            logSeverity = CefSettings.LogSeverity.LOGSEVERITY_DISABLE;

        return dh.onConsoleMessage(browser, logSeverity, message, source, line);
    }

    //
    // CefKeyboardHandler (all methods will be called on the UI thread).
    //

    @Override
    public boolean KeyboardHandler_OnPreKeyEvent(int bid, KeyEvent event) throws TException {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return false;
        CefKeyboardHandler kh = browser.getOwner().getKeyboardHandler();
        if (kh == null) return false;

        return kh.onPreKeyEvent(browser, thrift2jcef(event), new BoolRef());
    }

    private static CefKeyboardHandler.CefKeyEvent thrift2jcef(KeyEvent event) {
        if (event == null)
            return null;
        if (event.type == null || event.type.isEmpty()) {
            CefLog.Error("Empty key event type: ");
            return null;
        }

        CefKeyboardHandler.CefKeyEvent.EventType type;
        try {
            type = CefKeyboardHandler.CefKeyEvent.EventType.valueOf(event.type);
        } catch (Throwable e) {
            CefLog.Error("Unknown key event type: event.type=%s, exception: %s", event.type, e.getMessage());
            return null;
        }

        return new CefKeyboardHandler.CefKeyEvent(type, event.modifiers, event.windows_key_code, event.native_key_code, event.is_system_key, (char)event.character, (char)event.unmodified_character, event.focus_on_editable_field);
    }

    @Override
    public boolean KeyboardHandler_OnKeyEvent(int bid, KeyEvent event) throws TException {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return false;
        CefKeyboardHandler kh = browser.getOwner().getKeyboardHandler();
        if (kh == null) return false;

        return kh.onKeyEvent(browser, thrift2jcef(event));
    }

    //
    // CefFocusHandler (all methods will be called on the UI thread).
    //

    @Override
    public void FocusHandler_OnTakeFocus(int bid, boolean next) throws TException {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return;
        CefFocusHandler fh = browser.getOwner().getFocusHandler();
        if (fh == null) return;

        fh.onTakeFocus(browser, next);
    }

    @Override
    public boolean FocusHandler_OnSetFocus(int bid, String source) throws TException {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return false;
        CefFocusHandler fh = browser.getOwner().getFocusHandler();
        if (fh == null) return false;

        CefFocusHandler.FocusSource src = CefFocusHandler.FocusSource.FOCUS_SOURCE_NAVIGATION;
        if (source != null && !source.isEmpty()) {
            try {
                src = CefFocusHandler.FocusSource.valueOf(source);
            } catch (Throwable e) {
                CefLog.Error("FocusHandler_OnSetFocus: source=%s, exception: %s", source, e.getMessage());
            }
        }

        return fh.onSetFocus(browser, src);
    }

    @Override
    public void FocusHandler_OnGotFocus(int bid) throws TException {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return;
        CefFocusHandler fh = browser.getOwner().getFocusHandler();
        if (fh == null) return;

        fh.onGotFocus(browser);
    }

    //
    // CefRequestHandler
    //

    ///
    /// Called on the UI thread before browser navigation. Return true to cancel
    /// the navigation or false to allow the navigation to proceed. The |request|
    /// object cannot be modified in this callback.
    /// CefLoadHandler::OnLoadingStateChange will be called twice in all cases.
    /// If the navigation is allowed CefLoadHandler::OnLoadStart and
    /// CefLoadHandler::OnLoadEnd will be called. If the navigation is canceled
    /// CefLoadHandler::OnLoadError will be called with an |errorCode| value of
    /// ERR_ABORTED. The |user_gesture| value will be true if the browser
    /// navigated via explicit user gesture (e.g. clicking a link) or false if it
    /// navigated automatically (e.g. via the DomContentLoaded event).
    ///
    /*--cef()--*/
    @Override
    public boolean RequestHandler_OnBeforeBrowse(int bid, RObject frame, RObject request, boolean user_gesture, boolean is_redirect) {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return false;
        CefRequestHandler rh = browser.getOwner().getRequestHandler();
        if (rh == null) return false;

        RemoteRequestImpl rr = new RemoteRequestImpl(myRpc, request);
        RemoteFrame rframe = new RemoteFrame(myRpc, frame);
        return rh.onBeforeBrowse(browser, rframe, new RemoteRequest(rr), user_gesture, is_redirect);
    }

    private static final RObject NULL_ROBJECT = new RObject();

    ///
    /// Called on the browser process IO thread before a resource request is
    /// initiated. The |browser| and |frame| values represent the source of the
    /// request. |request| represents the request contents and cannot be modified
    /// in this callback. |is_navigation| will be true if the resource request is
    /// a navigation. |is_download| will be true if the resource request is a
    /// download. |request_initiator| is the origin (scheme + domain) of the page
    /// that initiated the request. Set |disable_default_handling| to true to
    /// disable default handling of the request, in which case it will need to be
    /// handled via CefResourceRequestHandler::GetResourceHandler or it will be
    /// canceled. To allow the resource load to proceed with default handling
    /// return NULL. To specify a handler for the resource return a
    /// CefResourceRequestHandler object. If this callback returns NULL the same
    /// method will be called on the associated CefRequestContextHandler, if any.
    ///
    @Override
    public RObject RequestHandler_GetResourceRequestHandler(int bid, RObject frame, RObject request, boolean isNavigation, boolean isDownload, String requestInitiator) {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return NULL_ROBJECT;
        CefRequestHandler rh = browser.getOwner().getRequestHandler();
        if (rh == null) return NULL_ROBJECT;

        RemoteRequestImpl rr = new RemoteRequestImpl(myRpc, request);
        RemoteFrame rframe = new RemoteFrame(myRpc, frame);
        BoolRef disableDefaultHandling = new BoolRef(false);
        CefResourceRequestHandler handler = rh.getResourceRequestHandler(browser, rframe, new RemoteRequest(rr), isNavigation, isDownload, requestInitiator, disableDefaultHandling);
        if (handler == null) return NULL_ROBJECT;

        RemoteResourceRequestHandler resultHandler = RemoteResourceRequestHandler.create(handler);
        return resultHandler.thriftId(disableDefaultHandling.get() ? 1 : 0);
    }

    ///
    /// Called on the IO thread before a resource request is loaded. The |browser|
    /// and |frame| values represent the source of the request, and may be NULL
    /// for requests originating from service workers or CefURLRequest. To
    /// optionally filter cookies for the request return a CefCookieAccessFilter
    /// object. The |request| object cannot not be modified in this callback.
    ///
    /*--cef(optional_param=browser,optional_param=frame)--*/
    @Override
    public RObject ResourceRequestHandler_GetCookieAccessFilter(int rrHandler, int bid, RObject frame, RObject request) {
        RemoteResourceRequestHandler rrrh = RemoteResourceRequestHandler.FACTORY.get(rrHandler);
        if (rrrh == null) return NULL_ROBJECT;

        RemoteRequestImpl rr = new RemoteRequestImpl(myRpc, request);
        RemoteFrame rframe = new RemoteFrame(myRpc, frame);
        CefCookieAccessFilter filter = null;
        try {
            rrrh.getDelegate().getCookieAccessFilter(getRemoteBrowser(bid), rframe, new RemoteRequest(rr));
        } catch (Throwable e) {
            CefLog.Error("getCookieAccessFilter: exception=%s", e.getMessage());
        }
        if (filter == null) return NULL_ROBJECT;
        RemoteCookieAccessFilter resultHandler = RemoteCookieAccessFilter.create(filter);
        return resultHandler.thriftId();
    }

    @Override
    public void ResourceRequestHandler_Dispose(int rrHandler)  {
        RemoteResourceRequestHandler.FACTORY.dispose(rrHandler);
    }

    @Override
    public void CookieAccessFilter_Dispose(int filter)  {
        RemoteCookieAccessFilter.FACTORY.dispose(filter);
    }

    private static CefCookie cookieFromList(List<String> cookie) {
        try {
            return new CefCookie(
                    cookie.get(0),
                    cookie.get(1),
                    cookie.get(2),
                    cookie.get(3),
                    Boolean.parseBoolean(cookie.get(4)),
                    Boolean.parseBoolean(cookie.get(5)),
                    new Date(Long.parseLong(cookie.get(6))),
                    new Date(Long.parseLong(cookie.get(7))),
                    Boolean.parseBoolean(cookie.get(8)),
                    new Date(Long.parseLong(cookie.get(9)))
            );
        } catch (NumberFormatException e) {
            CefLog.Error("Can't parse cookie: err %s, list: '%s'", e.getMessage(), Arrays.toString(cookie.toArray()));
            return null;
        }
    }

    ///
    /// Called on the IO thread before a resource request is sent. The |browser|
    /// and |frame| values represent the source of the request, and may be NULL
    /// for requests originating from service workers or CefURLRequest. |request|
    /// cannot be modified in this callback. Return true if the specified cookie
    /// can be sent with the request or false otherwise.
    ///
    /*--cef(optional_param=browser,optional_param=frame)--*/
    @Override
    public boolean CookieAccessFilter_CanSendCookie(int filter, int bid, RObject frame, RObject request, List<String> cookie)  {
        RemoteCookieAccessFilter f = RemoteCookieAccessFilter.FACTORY.get(filter);
        if (f == null) return false;

        RemoteRequestImpl rr = new RemoteRequestImpl(myRpc, request);
        RemoteFrame rframe = new RemoteFrame(myRpc, frame);
        boolean result = true;
        try {
            result = f.getDelegate().canSendCookie(getRemoteBrowser(bid), rframe, new RemoteRequest(rr), cookieFromList(cookie));
        } catch (Throwable e) {
            CefLog.Error("canSendCookie: exception=%s", e.getMessage());
        }
        return result;
    }

    ///
    /// Called on the IO thread after a resource response is received. The
    /// |browser| and |frame| values represent the source of the request, and may
    /// be NULL for requests originating from service workers or CefURLRequest.
    /// |request| cannot be modified in this callback. Return true if the
    /// specified cookie returned with the response can be saved or false
    /// otherwise.
    ///
    @Override
    public boolean CookieAccessFilter_CanSaveCookie(int filter, int bid, RObject frame, RObject request, RObject response, List<String> cookie)  {
        RemoteCookieAccessFilter f = RemoteCookieAccessFilter.FACTORY.get(filter);
        if (f == null) return false;

        RemoteRequestImpl rreq = new RemoteRequestImpl(myRpc, request);
        RemoteResponseImpl rresp = new RemoteResponseImpl(myRpc, response);
        RemoteFrame rframe = new RemoteFrame(myRpc, frame);
        boolean result = true;
        try {
            result = f.getDelegate().canSaveCookie(getRemoteBrowser(bid), rframe, new RemoteRequest(rreq), new RemoteResponse(rresp), cookieFromList(cookie));
        } catch (Throwable e) {
            CefLog.Error("canSaveCookie: exception=%s", e.getMessage());
        }
        // NOTE: doc doesn't say that response can't be modifed, but call rresp.flush() triggers cooresponding
        // error on server (i.e. resp is immutable) so don't do that.
        return result;
    }

    @Override
    public boolean RequestHandler_OnOpenURLFromTab(int bid, RObject frame, String target_url, boolean user_gesture) {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return false;
        CefRequestHandler rh = browser.getOwner().getRequestHandler();
        if (rh == null) return false;

        RemoteFrame rframe = new RemoteFrame(myRpc, frame);
        return rh.onOpenURLFromTab(browser, rframe, target_url, user_gesture);
    }

    @Override
    public boolean RequestHandler_GetAuthCredentials(int bid, String origin_url, boolean isProxy, String host, int port, String realm, String scheme, RObject authCallback) {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return false;
        CefRequestHandler rh = browser.getOwner().getRequestHandler();
        if (rh == null) return false;

        CefAuthCallback callback = new RemoteAuthCallback(myRpc, authCallback);
        return rh.getAuthCredentials(browser, origin_url, isProxy, host, port, realm, scheme, callback);
    }

    @Override
    public boolean RequestHandler_OnCertificateError(int bid, String cert_error, String request_url, ByteBuffer sslInfo, RObject callback) {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return false;
        CefRequestHandler rh = browser.getOwner().getRequestHandler();
        if (rh == null) return false;

        CefCallback cb = new RemoteCallback(myRpc, callback);
        CefSSLInfo ssl = RemoteSSLInfo.fromBinary(sslInfo);
        CefLoadHandler.ErrorCode err = CefLoadHandler.ErrorCode.ERR_NONE;
        if (cert_error != null && !cert_error.isEmpty()) {
            try {
                err = CefLoadHandler.ErrorCode.valueOf(cert_error);
            } catch (Throwable e) {
                CefLog.Error("OnCertificateError: cert_error=%s, exception: %s", cert_error, e.getMessage());
            }
        }
        return rh.onCertificateError(browser, err, request_url, ssl, cb);
    }

    @Override
    public void RequestHandler_OnRenderProcessTerminated(int bid, String status, int errCode, String errText) {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return;
        CefRequestHandler rh = browser.getOwner().getRequestHandler();
        if (rh == null) return;

        CefRequestHandler.TerminationStatus s = CefRequestHandler.TerminationStatus.TS_ABNORMAL_TERMINATION;
        if (status != null && !status.isEmpty()) {
            try {
                s = CefRequestHandler.TerminationStatus.valueOf(status);
            } catch (Throwable e) {
                CefLog.Error("onRenderProcessTerminated: status=%s, exception: %s", status, e.getMessage());
            }
        }
        rh.onRenderProcessTerminated(browser, s, errCode, errText);
    }

    ///
    /// Called on the IO thread before a resource request is loaded. The |browser|
    /// and |frame| values represent the source of the request, and may be NULL
    /// for requests originating from service workers or CefURLRequest. To
    /// redirect or change the resource load optionally modify |request|.
    /// Modification of the request URL will be treated as a redirect. Return
    /// RV_CONTINUE to continue the request immediately. Return RV_CONTINUE_ASYNC
    /// and call CefCallback methods at a later time to continue or cancel the
    /// request asynchronously. Return RV_CANCEL to cancel the request
    /// immediately.
    ///
    /*--cef(optional_param=browser,optional_param=frame, default_retval=RV_CONTINUE)--*/
    @Override
    public boolean ResourceRequestHandler_OnBeforeResourceLoad(int rrHandler, int bid, RObject frame, RObject request) {
        RemoteResourceRequestHandler rrrh = RemoteResourceRequestHandler.FACTORY.get(rrHandler);
        if (rrrh == null) return false;

        RemoteRequestImpl rr = new RemoteRequestImpl(myRpc, request);
        RemoteFrame rframe = new RemoteFrame(myRpc, frame);
        boolean result = false;
        try {
            result = rrrh.getDelegate().onBeforeResourceLoad(getRemoteBrowser(bid), rframe, new RemoteRequest(rr));
            rr.flush();
        } catch (Throwable e) {
            CefLog.Error("onBeforeResourceLoad: exception=%s", e.getMessage());
        }
        return result;
    }

    ///
    /// Called on the IO thread before a resource is loaded. The |browser| and
    /// |frame| values represent the source of the request, and may be NULL for
    /// requests originating from service workers or CefURLRequest. To allow the
    /// resource to load using the default network loader return NULL. To specify
    /// a handler for the resource return a CefResourceHandler object. The
    /// |request| object cannot not be modified in this callback.
    ///
    /*--cef(optional_param=browser,optional_param=frame)--*/
    @Override
    public RObject ResourceRequestHandler_GetResourceHandler(int rrHandler, int bid, RObject frame, RObject request) {
        RemoteResourceRequestHandler rrrh = RemoteResourceRequestHandler.FACTORY.get(rrHandler);
        if (rrrh == null) return NULL_ROBJECT;

        RemoteRequestImpl rr = new RemoteRequestImpl(myRpc, request);
        RemoteFrame rframe = new RemoteFrame(myRpc, frame);
        CefResourceHandler handler = null;
        try {
            handler = rrrh.getDelegate().getResourceHandler(getRemoteBrowser(bid), rframe, new RemoteRequest(rr));
        } catch (Throwable e) {
            CefLog.Error("getResourceHandler: exception=%s", e.getMessage());
        }

        if (handler == null) return NULL_ROBJECT;

        RemoteResourceHandler result = RemoteResourceHandler.create(handler);
        return result.thriftId();
    }

    ///
    /// Begin processing the request. To handle the request return true and call
    /// CefCallback::Continue() once the response header information is available
    /// (CefCallback::Continue() can also be called from inside this method if
    /// header information is available immediately). To cancel the request return
    /// false.
    ///
    /// WARNING: This method is deprecated. Use Open instead.
    ///
    @Override
    public boolean ResourceHandler_ProcessRequest(int resourceHandler, RObject request, RObject callback) throws TException {
        RemoteResourceHandler rrh = RemoteResourceHandler.FACTORY.find(resourceHandler);
        if (rrh == null) return false;

        RemoteRequestImpl rr = new RemoteRequestImpl(myRpc, request);
        CefCallback cb = new RemoteCallback(myRpc, callback);
        boolean result = false;
        try {
            result = rrh.getDelegate().processRequest(new RemoteRequest(rr), cb);
        } catch (Throwable e) {
            CefLog.Error("processRequest: exception=%s", e.getMessage());
        }
        // From java doc: the request cannot be modified in this callback. Instance only valid within the scope of this method.
        // So don't call rr.flush() here
        return result;
     }

    ///
    /// Retrieve response header information. If the response length is not known
    /// set |response_length| to -1 and ReadResponse() will be called until it
    /// returns false. If the response length is known set |response_length|
    /// to a positive value and ReadResponse() will be called until it returns
    /// false or the specified number of bytes have been read. Use the |response|
    /// object to set the mime type, http status code and other optional header
    /// values. To redirect the request to a new URL set |redirectUrl| to the new
    /// URL. |redirectUrl| can be either a relative or fully qualified URL.
    /// It is also possible to set |response| to a redirect http status code
    /// and pass the new URL via a Location header. Likewise with |redirectUrl| it
    /// is valid to set a relative or fully qualified URL as the Location header
    /// value. If an error occured while setting up the request you can call
    /// SetError() on |response| to indicate the error condition.
    ///
    @Override
    public ResponseHeaders ResourceHandler_GetResponseHeaders(int resourceHandler, RObject response) throws TException {
        RemoteResourceHandler rrh = RemoteResourceHandler.FACTORY.find(resourceHandler);
        if (rrh == null) return null;

        RemoteResponseImpl rr = new RemoteResponseImpl(myRpc, response);
        IntRef respLen = new IntRef();
        StringRef redirectUrlRef = new StringRef();
        ResponseHeaders result = null;
        try {
            rrh.getDelegate().getResponseHeaders(new RemoteResponse(rr), respLen, redirectUrlRef);
            rr.flush();
            result = new ResponseHeaders();
            result.setLength(respLen.get());
            if (redirectUrlRef.get() != null)
                result.setRedirectUrl(redirectUrlRef.get());
        } catch (Throwable e) {
            CefLog.Error("getResponseHeaders: exception=%s", e.getMessage());
        }
        return result;
    }

    /**
     * Read response data. If data is available immediately copy up to |bytesToRead| bytes into
     * |dataOut|, set |bytesRead| to the number of bytes copied, and return true. To read the data
     * at a later time set |bytesRead| to 0, return true and call CefCallback.Continue() when the
     * data is available. To indicate response completion return false.
     * @return True if more data is or will be available.
     */
    @Override
    public ResponseData ResourceHandler_ReadResponse(int resourceHandler, int bytes_to_read, RObject callback) throws TException {
        if (bytes_to_read <= 0) return new ResponseData();
        if (bytes_to_read > 256*1024)
            CefLog.Error("ResourceHandler_ReadResponse: too much bytes to read %d. Need to implement via shared memory.", bytes_to_read);

        RemoteResourceHandler rrh = RemoteResourceHandler.FACTORY.find(resourceHandler);
        if (rrh == null) return null;

        CefCallback cb = new RemoteCallback(myRpc, callback);

        byte[] buf = new byte[bytes_to_read];
        IntRef bytesRead = new IntRef();
        boolean continueRead = false;
        try {
            continueRead = rrh.getDelegate().readResponse(buf, bytes_to_read, bytesRead, cb);
        } catch (Throwable e) {
            CefLog.Error("readResponse: exception=%s", e.getMessage());
        }

        final int read = bytesRead.get();
        ResponseData result = new ResponseData();
        result.setContinueRead(continueRead);
        result.setBytes_read(read);
        result.setData(ByteBuffer.wrap(buf, 0, read));
        return result;
    }

    @Override
    public void ResourceHandler_Cancel(int resourceHandler) throws TException {
        RemoteResourceHandler rrh = RemoteResourceHandler.FACTORY.find(resourceHandler);
        if (rrh == null) return;

        try {
            rrh.getDelegate().cancel();
        } catch (Throwable e) {
            CefLog.Error("ResourceHandler_Cancel: exception=%s", e.getMessage());
        }
    }

    ///
    /// Called on the IO thread when a resource load is redirected. The |browser|
    /// and |frame| values represent the source of the request, and may be NULL
    /// for requests originating from service workers or CefURLRequest. The
    /// |request| parameter will contain the old URL and other request-related
    /// information. The |response| parameter will contain the response that
    /// resulted in the redirect. The |new_url| parameter will contain the new URL
    /// and can be changed if desired. The |request| and |response| objects cannot
    /// be modified in this callback.
    ///
    /*--cef(optional_param=browser,optional_param=frame)--*/
    @Override
    public String ResourceRequestHandler_OnResourceRedirect(int rrHandler, int bid, RObject frame, RObject request, RObject response, String new_url) {
        RemoteResourceRequestHandler rrrh = RemoteResourceRequestHandler.FACTORY.get(rrHandler);
        if (rrrh == null) return "";

        RemoteRequestImpl rreq = new RemoteRequestImpl(myRpc, request);
        RemoteResponseImpl rresp = new RemoteResponseImpl(myRpc, response);
        StringRef sref = new StringRef(new_url);
        RemoteFrame rframe = new RemoteFrame(myRpc, frame);
        try {
            rrrh.getDelegate().onResourceRedirect(getRemoteBrowser(bid), rframe, new RemoteRequest(rreq), new RemoteResponse(rresp), sref);
        } catch (Throwable e) {
            CefLog.Error("onResourceRedirect: exception=%s", e.getMessage());
        }
        return sref.get();
    }

    ///
    /// Called on the IO thread when a resource response is received. The
    /// |browser| and |frame| values represent the source of the request, and may
    /// be NULL for requests originating from service workers or CefURLRequest. To
    /// allow the resource load to proceed without modification return false. To
    /// redirect or retry the resource load optionally modify |request| and return
    /// true. Modification of the request URL will be treated as a redirect.
    /// Requests handled using the default network loader cannot be redirected in
    /// this callback. The |response| object cannot be modified in this callback.
    ///
    /// WARNING: Redirecting using this method is deprecated. Use
    /// OnBeforeResourceLoad or GetResourceHandler to perform redirects.
    ///
    /*--cef(optional_param=browser,optional_param=frame)--*/
    @Override
    public boolean ResourceRequestHandler_OnResourceResponse(int rrHandler, int bid, RObject frame, RObject request, RObject response) {
        RemoteResourceRequestHandler rrrh = RemoteResourceRequestHandler.FACTORY.get(rrHandler);
        if (rrrh == null) return false;

        RemoteRequestImpl rreq = new RemoteRequestImpl(myRpc, request);
        RemoteResponseImpl rresp = new RemoteResponseImpl(myRpc, response);
        RemoteFrame rframe = new RemoteFrame(myRpc, frame);
        boolean result = false;
        try {
            result = rrrh.getDelegate().onResourceResponse(getRemoteBrowser(bid), rframe, new RemoteRequest(rreq), new RemoteResponse(rresp));
            rreq.flush(); // |response| object cannot be modified in this callback.
        } catch (Throwable e) {
            CefLog.Error("onResourceResponse: exception=%s", e.getMessage());
        }
        return result;
    }

    ///
    /// Called on the IO thread when a resource load has completed. The |browser|
    /// and |frame| values represent the source of the request, and may be NULL
    /// for requests originating from service workers or CefURLRequest. |request|
    /// and |response| represent the request and response respectively and cannot
    /// be modified in this callback. |status| indicates the load completion
    /// status. |received_content_length| is the number of response bytes actually
    /// read. This method will be called for all requests, including requests that
    /// are aborted due to CEF shutdown or destruction of the associated browser.
    /// In cases where the associated browser is destroyed this callback may
    /// arrive after the CefLifeSpanHandler::OnBeforeClose callback for that
    /// browser. The CefFrame::IsValid method can be used to test for this
    /// situation, and care should be taken not to call |browser| or |frame|
    /// methods that modify state (like LoadURL, SendProcessMessage, etc.) if the
    /// frame is invalid.
    ///
    /*--cef(optional_param=browser,optional_param=frame)--*/
    @Override
    public void ResourceRequestHandler_OnResourceLoadComplete(int rrHandler, int bid, RObject frame, RObject request, RObject response, String status, long receivedContentLength) {
        RemoteResourceRequestHandler rrrh = RemoteResourceRequestHandler.FACTORY.get(rrHandler);
        if (rrrh == null) return;

        RemoteRequestImpl rreq = new RemoteRequestImpl(myRpc, request);
        RemoteResponseImpl rresp = new RemoteResponseImpl(myRpc, response);
        CefURLRequest.Status s = CefURLRequest.Status.UR_UNKNOWN;
        if (status != null && !status.isEmpty()) {
            try {
                s = CefURLRequest.Status.valueOf(status);
            } catch (Throwable e) {
                CefLog.Error("OnResourceLoadComplete: status=%s, exception: %s", status, e.getMessage());
            }
        }
        RemoteFrame rframe = new RemoteFrame(myRpc, frame);
        try {
            rrrh.getDelegate().onResourceLoadComplete(getRemoteBrowser(bid), rframe, new RemoteRequest(rreq), new RemoteResponse(rresp), s, receivedContentLength);
        } catch (Throwable e) {
            CefLog.Error("onResourceLoadComplete: exception=%s", e.getMessage());
        }
    }

    ///
    /// Called on the IO thread to handle requests for URLs with an unknown
    /// protocol component. The |browser| and |frame| values represent the source
    /// of the request, and may be NULL for requests originating from service
    /// workers or CefURLRequest. |request| cannot be modified in this callback.
    /// Set |allow_os_execution| to true to attempt execution via the registered
    /// OS protocol handler, if any. SECURITY WARNING: YOU SHOULD USE THIS METHOD
    /// TO ENFORCE RESTRICTIONS BASED ON SCHEME, HOST OR OTHER URL ANALYSIS BEFORE
    /// ALLOWING OS EXECUTION.
    ///
    /*--cef(optional_param=browser,optional_param=frame)--*/
    @Override
    public boolean ResourceRequestHandler_OnProtocolExecution(int rrHandler, int bid, RObject frame, RObject request, boolean allowOsExecution) {
        RemoteResourceRequestHandler rrrh = RemoteResourceRequestHandler.FACTORY.get(rrHandler);
        if (rrrh == null) return false;

        RemoteRequestImpl rreq = new RemoteRequestImpl(myRpc, request);
        RemoteFrame rframe = new RemoteFrame(myRpc, frame);
        BoolRef br = new BoolRef(allowOsExecution);
        try {
            rrrh.getDelegate().onProtocolExecution(getRemoteBrowser(bid), rframe, new RemoteRequest(rreq), br);
        } catch (Throwable e) {
            CefLog.Error("onProtocolExecution: exception=%s", e.getMessage());
        }
        return br.get();
    }

    @Override
    public List<MenuItem> ContextMenuHandler_OnBeforeContextMenu(int bid, RObject frame, ContextMenuParams params, List<MenuItem> menu_model) throws TException {
        RemoteBrowser cefBrowser = getRemoteBrowser(bid);
        if (cefBrowser == null) {
            CefLog.Error("CefContextMenuHandler::OnBeforeContextMenu: There is no browser with bid=%d", bid);
            return menu_model;
        }
        RemoteFrame cefFrame = new RemoteFrame(myRpc, frame);
        RemoteMenuModel cefMenuModel = new RemoteMenuModel(menu_model);
        CefContextMenuParams cefParams = new com.jetbrains.cef.remote.menu.ContextMenuParams(params);

        CefContextMenuHandler handler = cefBrowser.getOwner().getContextMenuHandler();
        if (handler == null) {
            CefLog.Error("CefContextMenuHandler::OnBeforeContextMenu: There is no CefContextMenuHandler for the browser with bid=%d", bid);
            return menu_model;
        }
        handler.onBeforeContextMenu(cefBrowser, cefFrame, cefParams, cefMenuModel);

        return cefMenuModel.getThriftModel();
    }

    @Override
    public boolean ContextMenuHandler_RunContextMenu(int bid, RObject frame, ContextMenuParams params, List<MenuItem> model, RObject callback) throws TException {
        RemoteBrowser cefBrowser = getRemoteBrowser(bid);
        if (cefBrowser == null) {
            CefLog.Error("CefContextMenuHandler::RunContextMenu: There is no browser with bid=%d", bid);
            return false;
        }
        RemoteFrame cefFrame = new RemoteFrame(myRpc, frame);
        CefContextMenuParams cefParams = new com.jetbrains.cef.remote.menu.ContextMenuParams(params);
        RemoteMenuModel cefMenuModel = new RemoteMenuModel(model);
        CefRunContextMenuCallback cefCallback = new RemoteRunContextMenuCallback(myRpc, callback);

        CefContextMenuHandler handler = cefBrowser.getOwner().getContextMenuHandler();
        if (handler == null) {
            CefLog.Error("CefContextMenuHandler::OnBeforeContextMenu: There is no CefContextMenuHandler for the browser with bid=%d", bid);
            return false;
        }

        return handler.runContextMenu(cefBrowser, cefFrame, cefParams, cefMenuModel, cefCallback);
    }

    @Override
    public boolean ContextMenuHandler_OnContextMenuCommand(int bid, RObject frame, ContextMenuParams params, int command_id, int event_flags) throws TException {
        RemoteBrowser cefBrowser = getRemoteBrowser(bid);
        if (cefBrowser == null) {
            CefLog.Error("CefContextMenuHandler::RunContextMenu: There is no browser with bid=%d", bid);
            return false;
        }
        RemoteFrame cefFrame = new RemoteFrame(myRpc, frame);
        CefContextMenuParams cefParams = new com.jetbrains.cef.remote.menu.ContextMenuParams(params);
        CefContextMenuHandler handler = cefBrowser.getOwner().getContextMenuHandler();
        if (handler == null) {
            CefLog.Error("CefContextMenuHandler::OnBeforeContextMenu: There is no CefContextMenuHandler for the browser with bid=%d", bid);
            return false;
        }
        return handler.onContextMenuCommand(cefBrowser, cefFrame, cefParams, command_id, event_flags);
    }

    @Override
    public void ContextMenuHandler_OnContextMenuDismissed(int bid, RObject frame) throws TException {
        RemoteBrowser cefBrowser = getRemoteBrowser(bid);
        if (cefBrowser == null) {
            CefLog.Error("CefContextMenuHandler::RunContextMenu: There is no browser with bid=%d", bid);
            return;
        }
        RemoteFrame cefFrame = new RemoteFrame(myRpc, frame);
        CefContextMenuHandler handler = cefBrowser.getOwner().getContextMenuHandler();
        if (handler == null) {
            CefLog.Error("CefContextMenuHandler::OnBeforeContextMenu: There is no CefContextMenuHandler for the browser with bid=%d", bid);
            return;
        }

        handler.onContextMenuDismissed(cefBrowser, cefFrame);
    }

    @Override
    public void ResourceHandler_Dispose(int resHandler) throws TException {
        RemoteResourceHandler.FACTORY.dispose(resHandler);
    }

    @Override
    public boolean MessageRouterHandler_onQuery(RObject handler, int bid, RObject frame, long queryId, String request, boolean persistent, RObject queryCallback) throws TException {
        RemoteMessageRouterHandler rmrh = RemoteMessageRouterHandler.FACTORY.get(handler.objId);
        if (rmrh == null) return false;

        RemoteQueryCallback rcb = new RemoteQueryCallback(myRpc, queryCallback);
        RemoteFrame rframe = new RemoteFrame(myRpc, frame);
        boolean result = false;
        try {
            result = rmrh.getDelegate().onQuery(getRemoteBrowser(bid), rframe, queryId, request, persistent, rcb);
        } catch (Throwable e) {
            CefLog.Error("onQuery: exception=%s", e.getMessage());
        }
        return result;
    }

    @Override
    public void MessageRouterHandler_onQueryCanceled(RObject handler, int bid, RObject frame, long queryId) throws TException {
        RemoteMessageRouterHandler rmrh = RemoteMessageRouterHandler.FACTORY.get(handler.objId);
        if (rmrh == null) return;

        RemoteFrame rframe = new RemoteFrame(myRpc, frame);
        try {
            rmrh.getDelegate().onQueryCanceled(getRemoteBrowser(bid), rframe, queryId);
        } catch (Throwable e) {
            CefLog.Error("onQueryCanceled: exception=%s", e.getMessage());
        }
    }

    @Override
    public void MessageRouterHandler_Dispose(int handler) throws TException {
        RemoteMessageRouterHandler.FACTORY.dispose(handler);
    }

    @Override
    public RObject SchemeHandlerFactory_CreateHandler(int schemeHandlerFactory, int bid, RObject frame, String scheme_name, RObject request) throws TException {
        RemoteSchemeHandlerFactory sf = RemoteSchemeHandlerFactory.FACTORY.get(schemeHandlerFactory);
        if (sf == null) return NULL_ROBJECT;

        RemoteRequestImpl rreq = new RemoteRequestImpl(myRpc, request);
        RemoteFrame rframe = new RemoteFrame(myRpc, frame);
        CefResourceHandler handler = null;
        try {
            handler = sf.getDelegate().create(getRemoteBrowser(bid), rframe, scheme_name, new RemoteRequest(rreq));
        } catch (Throwable e) {
            CefLog.Error("SchemeHandlerFactory_CreateHandler: exception=%s", e.getMessage());
        }
        if (handler == null) return NULL_ROBJECT;

        RemoteResourceHandler result = RemoteResourceHandler.create(handler);
        return result.thriftId();
    }

    @Override
    public void SchemeHandlerFactory_Dispose(int schemeHandlerFactory) throws TException {
        RemoteSchemeHandlerFactory.FACTORY.dispose(schemeHandlerFactory);
    }

    @Override
    public void CompletionCallback_OnComplete(int completionCallback) throws TException {
        RemoteCompletionCallback cc = RemoteCompletionCallback.FACTORY.get(completionCallback);
        if (cc == null) return;

        try {
            cc.getDelegate().onComplete();
        } catch (Throwable e) {
            CefLog.Error("CompletionCallback_OnComplete: exception=%s", e.getMessage());
        }
        RemoteCompletionCallback.FACTORY.dispose(completionCallback);
    }

    @Override
    public RObject RequestContextHandler_GetResourceRequestHandler(int handlerId, int bid, RObject frame, RObject request, boolean isNavigation, boolean isDownload, String requestInitiator) throws TException {
        RemoteRequestContextHandler rhandler = RemoteRequestContextHandler.FACTORY.get(handlerId);
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null || rhandler == null) return NULL_ROBJECT;

        RemoteRequestImpl rr = new RemoteRequestImpl(myRpc, request);
        RemoteFrame rframe = new RemoteFrame(myRpc, frame);
        BoolRef disableDefaultHandling = new BoolRef(false);
        CefResourceRequestHandler handler = null;
        try {
            handler = rhandler.getDelegate().getResourceRequestHandler(browser, rframe, new RemoteRequest(rr), isNavigation, isDownload, requestInitiator, disableDefaultHandling);
        } catch (Throwable e) {
            CefLog.Error("getResourceRequestHandler: exception=%s", e.getMessage());
        }
        if (handler == null) return NULL_ROBJECT;

        RemoteResourceRequestHandler resultHandler = RemoteResourceRequestHandler.create(handler);
        return resultHandler.thriftId(disableDefaultHandling.get() ? 1 : 0);
    }

    @Override
    public boolean CookieVisitor_Visit(int visitor, Cookie cookie, int count, int total) throws TException {
        RemoteCookieVisitor rvisitor = RemoteCookieVisitor.FACTORY.get(visitor);
        if (rvisitor == null) return false;

        BoolRef delete = new BoolRef(false);
        boolean continueTraverse = true;
        try {
            continueTraverse = rvisitor.getDelegate().visit(RemoteCookieManager.toCefCookie(cookie), count, total, delete);
        } catch (Throwable e) {
            CefLog.Error("CookieVisitor_Visit: exception=%s", e.getMessage());
        }
        if (delete.get())
            CefLog.Error("Can't delete cookie %s via CefCookieVisitor, please use CefCookieManager.deleteCookie. TODO: implement.", cookie);
        if (count == total || !continueTraverse) {
            CefLog.Debug("Last cookie (%d) visited, dispose RemoteCookieVisitor %d.", total, visitor);
            RemoteCookieVisitor.FACTORY.dispose(visitor);
        }
        return continueTraverse;
    }

    @Override
    public void CookieVisitor_Dispose(int visitor) throws TException {
        CefLog.Debug("Dispose RemoteCookieVisitor %d (by server request).", visitor);
        RemoteCookieVisitor.FACTORY.dispose(visitor);
    }

    @Override
    public void StringVisitor_Visit(int visitor, String str) throws TException {
        RemoteStringVisitor rvisitor = RemoteStringVisitor.FACTORY.get(visitor);
        if (rvisitor == null) return;

        try {
            rvisitor.getDelegate().visit(str);
        } catch (Throwable e) {
            CefLog.Error("StringVisitor_Visit: exception=%s", e.getMessage());
        }
    }

    @Override
    public void StringVisitor_Dispose(int visitor) throws TException {
        CefLog.Debug("Dispose RemoteStringVisitor %d (by server request).", visitor);
        RemoteStringVisitor.FACTORY.dispose(visitor);
    }

    @Override
    public void DevToolsMessageObserver_OnDevToolsEvent(int observer, int bid, String method, String parameters) throws TException {
        RemoteDevToolsMessageObserver ro = RemoteDevToolsMessageObserver.FACTORY.get(observer);
        if (ro == null) return;
        try {
            ro.getDelegate().onDevToolsEvent(getRemoteBrowser(bid), method, parameters);
        } catch (Throwable e) {
            CefLog.Error("onDevToolsEvent: exception=%s", e.getMessage());
        }
    }

    @Override
    public boolean PermissionHandler_OnRequestMediaAccessPermission(int bid, RObject frame, String requesting_origin, int requested_permissions, RObject mediaAccessCallback) throws TException {
        RemoteBrowser browser = getRemoteBrowser(bid);
        if (browser == null) return false;
        CefPermissionHandler permissionHandler = browser.getOwner().getPermissionHandler();
        if (permissionHandler == null) return false;
        RemoteFrame remoteFrame = frame.isNull ? null : new RemoteFrame(myRpc, frame);
        return permissionHandler.onRequestMediaAccessPermission(browser, remoteFrame, requesting_origin, requested_permissions, new RemoteMediaAccessCallback(myRpc, mediaAccessCallback));
    }

    @Override
    public void DevToolsMessageObserver_OnDevToolsMethodResult(int observer, int bid, int messageId, boolean success, String result) throws TException {
        RemoteDevToolsMessageObserver ro = RemoteDevToolsMessageObserver.FACTORY.get(observer);
        if (ro == null) return;
        try {
            ro.getDelegate().onDevToolsMethodResult(getRemoteBrowser(bid), messageId, success, result);
        } catch (Throwable e) {
            CefLog.Error("onDevToolsMethodResult: exception=%s", e.getMessage());
        }
    }

    @Override
    public void DevToolsMessageObserver_Dispose(int observer) throws TException {
        RemoteDevToolsMessageObserver.FACTORY.dispose(observer);
    }

    @Override
    public void IntCallback_OnComplete(int intCallback, int result) throws TException {
        RemoteIntCallback rcb = RemoteIntCallback.FACTORY.get(intCallback);
        if (rcb == null) return;

        rcb.onComplete(result);
        RemoteIntCallback.FACTORY.dispose(intCallback);
    }

    @Override
    public void RunFileDialogCallback_OnFileDialogDismissed(int runFileDialogCallback, List<String> filePaths) throws TException {
        RemoteRunFileDialogCallback rcb = RemoteRunFileDialogCallback.FACTORY.get(runFileDialogCallback);
        if (rcb == null) return;

        try {
            rcb.getDelegate().onFileDialogDismissed(new Vector(filePaths));
        } catch (Throwable e) {
            CefLog.Error("onFileDialogDismissed: exception=%s", e.getMessage());
        }
        RemoteRunFileDialogCallback.FACTORY.dispose(runFileDialogCallback);
    }

    @Override
    public void PdfPrintCallback_OnPdfPrintFinished(int pdfPrintCallback, String path, boolean ok) throws TException {
        RemotePdfPrintCallback rcb = RemotePdfPrintCallback.FACTORY.get(pdfPrintCallback);
        if (rcb == null) return;

        try {
            rcb.getDelegate().onPdfPrintFinished(path, ok);
        } catch (Throwable e) {
            CefLog.Error("onPdfPrintFinished: exception=%s", e.getMessage());
        }
        RemotePdfPrintCallback.FACTORY.dispose(pdfPrintCallback);
    }
}
