///|
fn parse_navigation_request(json_str : String) -> NavigationRequest? {
  let parsed = @json.parse(json_str) catch { _ => return None }
  match parsed {
    Object(map) =>
      match
        (
          map.get("url"),
          map.get("http_method"),
          map.get("content_type"),
          map.get("body"),
        ) {
        (
          Some(String(url)),
          Some(String(http_method)),
          Some(String(content_type)),
          Some(String(body)),
        ) =>
          Some({ url, http_method: http_method.to_upper(), content_type, body })
        _ => None
      }
    _ => None
  }
}

///|
async fn Browser::drain_pending_form_submission_navigation(
  self : Browser,
) -> Bool raise @http.HttpError {
  let value = self.peek_pending_form_submission_navigation()
  match value {
    Some(value) =>
      if value.length() == 0 {
        false
      } else if value.has_prefix("request:") {
        match
          parse_navigation_request(
            value.unsafe_substring(start=8, end=value.length()),
          ) {
          Some(request) => {
            let _ = self.shift_pending_form_submission_navigation()
            let resolved_request = {
              ..request,
              url: resolve_url(self.current_url, request.url),
            }
            let _ = self.navigate_request(resolved_request)
            true
          }
          None => {
            let _ = self.shift_pending_form_submission_navigation()
            self.drain_pending_form_submission_navigation()
          }
        }
      } else {
        let _ = self.shift_pending_form_submission_navigation()
        self.drain_pending_form_submission_navigation()
      }
    None => false
  }
}

///|
fn Browser::drain_pending_form_submission_navigation_sync(
  self : Browser,
) -> Bool {
  let value = self.peek_pending_form_submission_navigation()
  match value {
    Some(value) =>
      if value.length() == 0 {
        false
      } else if value.has_prefix("request:") {
        match
          parse_navigation_request(
            value.unsafe_substring(start=8, end=value.length()),
          ) {
          Some(request) => {
            let resolved_request = {
              ..request,
              url: resolve_url(self.current_url, request.url),
            }
            if self.navigate_sync_if_supported(resolved_request) {
              let _ = self.shift_pending_form_submission_navigation()
              true
            } else {
              false
            }
          }
          None => {
            let _ = self.shift_pending_form_submission_navigation()
            self.drain_pending_form_submission_navigation_sync()
          }
        }
      } else {
        let _ = self.shift_pending_form_submission_navigation()
        self.drain_pending_form_submission_navigation_sync()
      }
    None => false
  }
}

///|
fn browser_form_submit_bridge_source() -> String {
  let source =
    #|(() => {
    #|  const queue = (globalThis.__craterPendingFormSubmissions = []);
    #|  function isSubmitButtonElement(element) {
    #|    if (!element || !element.tagName || !element.getAttribute) return false;
    #|    const tag = String(element.tagName || '').toLowerCase();
    #|    const type = String(element.getAttribute('type') || '').toLowerCase();
    #|    if (tag === 'button') return type === '' || type === 'submit';
    #|    return tag === 'input' && (type === 'submit' || type === 'image');
    #|  }
    #|  function resolveFormOwnerForSubmitter(element) {
    #|    if (!element) return null;
    #|    if (element.form !== undefined && element.form !== null) return element.form;
    #|    return element.closest ? element.closest('form') : null;
    #|  }
    #|  function hasSubmitListenersInPath(target) {
    #|    let current = target;
    #|    while (current) {
    #|      const store = current._listeners && current._listeners.submit;
    #|      if (store && store.length > 0) return true;
    #|      current = current.parentNode || null;
    #|    }
    #|    const docStore = document && document._listeners && document._listeners.submit;
    #|    if (docStore && docStore.length > 0) return true;
    #|    const winStore = window && window._listeners && window._listeners.submit;
    #|    return !!(winStore && winStore.length > 0);
    #|  }
    #|  function buildFormNavigationRequest(form, submitter, clientX, clientY) {
    #|    if (!form) return 'none';
    #|    const submitMethodAttr = submitter && submitter.getAttribute ? submitter.getAttribute('formmethod') : null;
    #|    const methodSource = submitMethodAttr === null ? ((form.getAttribute && form.getAttribute('method')) || 'get') : String(submitMethodAttr);
    #|    const method = methodSource ? String(methodSource).toLowerCase() : 'get';
    #|    const submitTargetAttr = submitter && submitter.getAttribute ? submitter.getAttribute('formtarget') : null;
    #|    const targetSource = submitTargetAttr === null ? ((form.getAttribute && form.getAttribute('target')) || '') : String(submitTargetAttr);
    #|    const normalizedTarget = targetSource ? String(targetSource).toLowerCase() : '';
    #|    if (normalizedTarget !== '' && normalizedTarget !== '_self' && normalizedTarget !== '_top' && normalizedTarget !== '_parent') return 'handled';
    #|    const submitActionAttr = submitter && submitter.getAttribute ? submitter.getAttribute('formaction') : null;
    #|    let action = submitActionAttr === null ? ((form.getAttribute && form.getAttribute('action')) || '') : String(submitActionAttr);
    #|    if (!action) action = (document && document.URL) || ((window && window.location && window.location.href) || '');
    #|    if (!action) return 'handled';
    #|    const submitEnctypeAttr = submitter && submitter.getAttribute ? submitter.getAttribute('formenctype') : null;
    #|    const enctypeSource = submitEnctypeAttr === null ? ((form.getAttribute && form.getAttribute('enctype')) || 'application/x-www-form-urlencoded') : String(submitEnctypeAttr);
    #|    const enctype = enctypeSource ? String(enctypeSource).toLowerCase() : 'application/x-www-form-urlencoded';
    #|    const encodedPairs = [];
    #|    const plainPairs = [];
    #|    function pushPair(name, value) {
    #|      const stringName = String(name);
    #|      const stringValue = String(value);
    #|      encodedPairs.push(encodeURIComponent(stringName) + '=' + encodeURIComponent(stringValue));
    #|      plainPairs.push(stringName + '=' + stringValue);
    #|    }
    #|    function pushField(element) {
    #|      if (!element || !element.getAttribute) return;
    #|      const name = element.getAttribute('name');
    #|      if (!name) return;
    #|      const tag = String(element.tagName || '').toLowerCase();
    #|      const type = String(element.getAttribute('type') || '').toLowerCase();
    #|      if (tag === 'input' && (type === 'submit' || type === 'button' || type === 'image' || type === 'reset' || type === 'file')) return;
    #|      if ((type === 'checkbox' || type === 'radio') && !element.checked) return;
    #|      const value = element.value !== undefined && element.value !== null ? String(element.value) : String(element.getAttribute('value') || '');
    #|      pushPair(name, value);
    #|    }
    #|    function walk(node) {
    #|      if (!node || !node.childNodes) return;
    #|      for (let i = 0; i < node.childNodes.length; i++) {
    #|        const child = node.childNodes[i];
    #|        if (!child || child.nodeType !== 1) continue;
    #|        const childTag = String(child.tagName || '').toLowerCase();
    #|        if (childTag === 'input' || childTag === 'select' || childTag === 'textarea') pushField(child);
    #|        walk(child);
    #|      }
    #|    }
    #|    walk(form);
    #|    if (submitter && submitter.getAttribute) {
    #|      const submitTag = String(submitter.tagName || '').toLowerCase();
    #|      const submitType = String(submitter.getAttribute('type') || '').toLowerCase();
    #|      const submitName = submitter.getAttribute('name');
    #|      if (submitTag === 'input' && submitType === 'image') {
    #|        const rect = submitter.getBoundingClientRect ? submitter.getBoundingClientRect() : { left: 0, top: 0 };
    #|        const imageX = Math.max(0, Math.floor(Number(clientX) - (Number(rect.left) || 0)));
    #|        const imageY = Math.max(0, Math.floor(Number(clientY) - (Number(rect.top) || 0)));
    #|        const prefix = submitName ? String(submitName) + '.' : '';
    #|        pushPair(prefix + 'x', imageX);
    #|        pushPair(prefix + 'y', imageY);
    #|      } else if (submitName) {
    #|        const submitValue = submitter.value !== undefined && submitter.value !== null ? String(submitter.value) : String(submitter.getAttribute('value') || '');
    #|        pushPair(submitName, submitValue);
    #|      }
    #|    }
    #|    if (method === 'get') {
    #|      if (encodedPairs.length > 0) action += (action.indexOf('?') >= 0 ? '&' : '?') + encodedPairs.join('&');
    #|      return 'request:' + JSON.stringify({ url: action, http_method: 'GET', content_type: '', body: '' });
    #|    }
    #|    let contentType = 'application/x-www-form-urlencoded;charset=UTF-8';
    #|    let body = encodedPairs.join('&');
    #|    if (enctype === 'text/plain') {
    #|      contentType = 'text/plain;charset=UTF-8';
    #|      body = plainPairs.join('\r\n');
    #|    } else if (enctype === 'multipart/form-data') {
    #|      const boundary = '----CraterFormBoundary';
    #|      contentType = 'multipart/form-data; boundary=' + boundary;
    #|      body = '';
    #|      for (const pair of plainPairs) {
    #|        const idx = pair.indexOf('=');
    #|        const partName = idx >= 0 ? pair.slice(0, idx) : pair;
    #|        const partValue = idx >= 0 ? pair.slice(idx + 1) : '';
    #|        body += '--' + boundary + '\r\n';
    #|        body += 'Content-Disposition: form-data; name="' + partName.replace(/"/g, '%22') + '"\r\n\r\n';
    #|        body += partValue + '\r\n';
    #|      }
    #|      body += '--' + boundary + '--\r\n';
    #|    }
    #|    return 'request:' + JSON.stringify({ url: action, http_method: method.toUpperCase(), content_type: contentType, body });
    #|  }
    #|  function shouldSkipValidation(form, submitter) {
    #|    if (submitter && submitter.hasAttribute && submitter.hasAttribute('formnovalidate')) return true;
    #|    return !!(form && form.hasAttribute && form.hasAttribute('novalidate'));
    #|  }
    #|  function dispatchFormSubmitThroughBridge(form, submitter, clientX, clientY) {
    #|    if (!form) return 'none';
    #|    if (!shouldSkipValidation(form, submitter) &&
    #|        typeof form.reportValidity === 'function' &&
    #|        !form.reportValidity()) {
    #|      return 'invalid';
    #|    }
    #|    const event = new SubmitEvent('submit', { bubbles: true, cancelable: true, submitter });
    #|    if (!form.dispatchEvent(event) && hasSubmitListenersInPath(form)) return 'prevent';
    #|    return buildFormNavigationRequest(form, submitter, clientX, clientY);
    #|  }
    #|  function submitThroughBridge() {
    #|    const result = buildFormNavigationRequest(this, null, 0, 0);
    #|    if (result !== 'none' && result !== 'prevent') queue.push(result);
    #|    return undefined;
    #|  }
    #|  function requestSubmitThroughBridge(submitter) {
    #|    const normalizedSubmitter = submitter === undefined ? null : submitter;
    #|    if (normalizedSubmitter !== null) {
    #|      if (!isSubmitButtonElement(normalizedSubmitter)) {
    #|        throw new TypeError("Failed to execute 'requestSubmit' on 'HTMLFormElement': parameter 1 is not a submit button");
    #|      }
    #|      if (resolveFormOwnerForSubmitter(normalizedSubmitter) !== this) {
    #|        throw new DOMException("Failed to execute 'requestSubmit' on 'HTMLFormElement': the specified element is not owned by this form element", 'NotFoundError');
    #|      }
    #|    }
    #|    const result = dispatchFormSubmitThroughBridge(this, normalizedSubmitter, 0, 0);
    #|    if (result !== 'none' && result !== 'prevent' && result !== 'invalid') queue.push(result);
    #|    return undefined;
    #|  }
    #|  globalThis.__craterBuildFormNavigationRequest = buildFormNavigationRequest;
    #|  globalThis.__craterDispatchFormSubmit = dispatchFormSubmitThroughBridge;
    #|  Object.defineProperty(HTMLFormElement.prototype, 'submit', {
    #|    value: submitThroughBridge,
    #|    configurable: true,
    #|    writable: true,
    #|  });
    #|  Object.defineProperty(HTMLFormElement.prototype, 'requestSubmit', {
    #|    value: requestSubmitThroughBridge,
    #|    configurable: true,
    #|    writable: true,
    #|  });
    #|  if (document && typeof document.querySelectorAll === 'function') {
    #|    const forms = document.querySelectorAll('form');
    #|    for (let i = 0; i < forms.length; i++) {
    #|      const form = forms[i];
    #|      if (!form) continue;
    #|      Object.defineProperty(form, 'submit', {
    #|        value: submitThroughBridge,
    #|        configurable: true,
    #|        writable: true,
    #|      });
    #|      Object.defineProperty(form, 'requestSubmit', {
    #|        value: requestSubmitThroughBridge,
    #|        configurable: true,
    #|        writable: true,
    #|      });
    #|    }
    #|  }
    #|  globalThis.__craterHasSubmitListenersInPath = hasSubmitListenersInPath;
    #|  globalThis.__craterBrowserFormSubmitBridgeInstalled = true;
    #|  return 'ok';
    #|})()
  source
}

///|
fn browser_pending_form_submission_source() -> String {
  let source =
    #|(() => {
    #|  const queue = globalThis.__craterPendingFormSubmissions || [];
    #|  if (queue.length === 0) return JSON.stringify('');
    #|  const next = queue.shift();
    #|  return JSON.stringify(next === undefined || next === null ? '' : String(next));
    #|})()
  source
}

///|
fn browser_peek_pending_form_submission_source() -> String {
  let source =
    #|(() => {
    #|  const queue = globalThis.__craterPendingFormSubmissions || [];
    #|  if (queue.length === 0) return JSON.stringify('');
    #|  const next = queue[0];
    #|  return JSON.stringify(next === undefined || next === null ? '' : String(next));
    #|})()
  source
}

///|
fn Browser::peek_pending_form_submission_navigation(self : Browser) -> String? {
  match self.script_executor {
    Some(executor) =>
      decode_js_string_result(
        executor.execute_source(browser_peek_pending_form_submission_source()).value,
      ) catch {
        err => {
          println("[JS] Error: " + err.to_string())
          None
        }
      }
    None => None
  }
}

///|
fn Browser::shift_pending_form_submission_navigation(self : Browser) -> String? {
  match self.script_executor {
    Some(executor) =>
      decode_js_string_result(
        executor.execute_source(browser_pending_form_submission_source()).value,
      ) catch {
        err => {
          println("[JS] Error: " + err.to_string())
          None
        }
      }
    None => None
  }
}

///|
fn Browser::dispatch_submit_to_source_id(
  self : Browser,
  source_id : String,
  client_x? : Int = 0,
  client_y? : Int = 0,
) -> SubmitDispatchResult {
  if !self.enable_js {
    return NotSubmittable
  }
  let escaped = escape_js_string(source_id)
  let source_client_x = client_x.to_string()
  let source_client_y = client_y.to_string()
  let source = "(function(){const target=document.getElementById('" +
    escaped +
    "');if(!target){return 'none';}const clientX=" +
    source_client_x +
    ";const clientY=" +
    source_client_y +
    ";const tag=(target.tagName||'').toLowerCase();const type=(target.getAttribute?((target.getAttribute('type')||'').toLowerCase()):'');let form=null;let submitter=null;if(tag==='form'){form=target;}else{if(tag==='button'){if(type!==''&&type!=='submit'){return 'none';}submitter=target;}else if(tag==='input'){if(type==='submit'||type==='image'){submitter=target;}else if(type==='button'||type==='reset'||type==='checkbox'||type==='radio'||type==='file'){return 'none';}}else if(tag==='textarea'||tag==='select'){return 'none';}form=target.closest?target.closest('form'):null;}if(!form){return 'none';}const dispatchSubmit=globalThis.__craterDispatchFormSubmit;if(typeof dispatchSubmit==='function'){return dispatchSubmit(form,submitter,clientX,clientY);}const build=globalThis.__craterBuildFormNavigationRequest;if(typeof build!=='function'){return 'handled';}return build(form,submitter,clientX,clientY);})()"
  match self.execute_inline_js(source) {
    Some(result) => {
      let _ = self.sync_render_state_from_dom_tree()
      if result == "none" {
        NotSubmittable
      } else if result == "prevent" {
        Prevented
      } else if result == "invalid" {
        Handled
      } else if result == "handled" {
        Handled
      } else if result.has_prefix("request:") {
        match
          parse_navigation_request(
            result.unsafe_substring(start=8, end=result.length()),
          ) {
          Some(request) => Navigate(request)
          None => NotSubmittable
        }
      } else {
        NotSubmittable
      }
    }
    None => NotSubmittable
  }
}