// Copyright (c) 2025 Huawei Device Co., Ltd. All rights reserved
// Use of this source code is governed by a Apache-2.0 license that can be
// found in the LICENSE file.

import { RNComponentContext } from '@rnoh/react-native-openharmony';
import webview from '@ohos.web.webview';
import { url as OSUrl } from '@kit.ArkTS';
import { RNC, TM } from './generated';
import Logger from './Logger';
import { BaseOperate } from './WebViewBaseOperate';
import {
  AlertEvent,
  CACHE_MODE,
  JAVASCRIPT_INTERFACE,
  ONE_HUNDRED,
  EIGHT,
  ResultType,
  RNCWebViewBridge,
  WebViewEventParams,
  WebViewNewSource,
  WebViewOverScrollMode,
  ZERO,
  COMMAND_NAME,
  WebViewDescriptor,
  REFRESH_OFFSET,
  MINHEIGHT,
} from './Magic';

import { bundleManager } from '@kit.AbilityKit';
import { WebViewTurboModule, ShouldStartParams, ShouldOverrideCallbackState } from './WebViewTurboModule';
import { AnyThreadRNInstance, WorkerTaskRunnable } from '@rnoh/react-native-openharmony/src/main/ets/RNOH';

export const TAG = "WebView"

export const WEB_VIEW = "RNCWebView"

@Sendable
class WorkerRunnable implements WorkerTaskRunnable<ShouldStartParams> {
  run(rnInstance: AnyThreadRNInstance, params: ShouldStartParams): void {
    rnInstance.getTurboModule<WebViewTurboModule>(TM.RNCWebViewModule.NAME).setShouldStartParams(params)
  }
}

@Component
export struct RNCWebView {
  public static readonly NAME = RNC.RNCWebView.NAME
  ctx!: RNComponentContext
  tag: number = ZERO
  source: WebViewNewSource = {
    uri: "",
    method: "",
    body: "",
    html: "",
    baseUrl: ""
  }
  html: string | undefined = "";
  url: string | Resource = "";
  controller: webview.WebviewController = new webview.WebviewController();
  javaScriptEnable: boolean = true;
  allowFileAccess: boolean = true;
  forceDark: boolean = true;
  minFontSize: number = EIGHT;
  overScrollMode: OverScrollMode = OverScrollMode.NEVER;
  progress: number = ZERO;
  cacheMode: number = CacheMode.Default;
  requestUrl: string = "";
  bundleName: string = "";
  messagingEnabled: boolean = false;
  hasRegisterJavaScriptProxy: boolean = false;
  controllerAttached: boolean = false;
  renderMode: RenderMode = RenderMode.SYNC_RENDER;
  scrollEnabled = true;
  nestedScroll = NestedScrollMode.SELF_FIRST;
  headers: Array<webview.WebHeader> = []
  injectedJavaScriptBeforeContentLoaded: Array<ScriptItem> = [
    { script: '', scriptRules: ["*"] }
  ];
  allowPageStartInProgress = true;
  @State webviewWidth: number = ZERO
  @State webviewHeight: number = ZERO
  @State isRefreshing: boolean = false
  @State mode: MixedMode = MixedMode.All;
  private unregisterDescriptorChangesListener?: () => void = undefined
  private cleanupCommandCallback?: () => void = undefined
  private eventEmitter: RNC.RNCWebView.EventEmitter | undefined = undefined
  private cleanUpCallbacks: (() => void)[] = []
  private descriptorWrapper: WebViewDescriptor = Object() as WebViewDescriptor
  private webViewBaseOperate: BaseOperate | null = null
  private shouldInterceptLoad: boolean = true
  private callLoadTimer: number = -1
  static readonly SHOULD_OVERRIDE_URL_LOADING_TIMEOUT: number = 250

  aboutToAppear() {
    try {
      this.initVariable()
      this.getBundleName()
      this.url = this.source.uri as string;
      this.onDescriptorWrapperChange(this.descriptorWrapper)
      this.unregisterDescriptorChangesListener = this.ctx.descriptorRegistry.subscribeToDescriptorChanges(this.tag,
        (newDescriptor) => {
          this.onDescriptorWrapperChange(newDescriptor as WebViewDescriptor)
        }
      )
      BaseOperate.setThirdPartyCookiesEnabled(this.descriptorWrapper.rawProps.thirdPartyCookiesEnabled)
      webview.WebviewController.setWebDebuggingAccess(this.descriptorWrapper.rawProps.webviewDebuggingEnabled)
      this.registerCommandCallback()
      if (this.hasIncognito()) {
        this.cacheMode = CacheMode.Online;
      }
      if (this.source.headers) {
        this.source.headers.forEach(item => {
          if (item.name && item.value) {
            this.headers.push({ headerKey: item.name, headerValue: item.value })
          }
        })
      }
    } catch (error) {
      Logger.error(TAG, `[RNOH]Errorcode: ${error.code}, Message: ${error.message}`);
    }

  }

  hasIncognito(): boolean {
    return this.descriptorWrapper.rawProps.incognito && this.descriptorWrapper.rawProps.incognito === true
  }

  createWebViewEvent(type: string): WebViewEventParams {
    return this.webViewBaseOperate?.createWebViewEvent({ type, progress: this.progress }) as WebViewEventParams;
  }

  aboutToDisappear() {
    this.getUIContext().getFocusController().clearFocus()
    this.cleanupCommandCallback?.()
    this.unregisterDescriptorChangesListener?.()
    this.cleanUpCallbacks.forEach(cb => cb())
    try {
      if (this.hasIncognito()) {
        this.webViewBaseOperate?.setIncognito()
      }
      this.controller.deleteJavaScriptRegister(JAVASCRIPT_INTERFACE)
      this.controller.refresh()
      this.hasRegisterJavaScriptProxy = false
      this.webViewBaseOperate = null
    } catch (error) {
      Logger.error(TAG, `[RNOH]Errorcode: ${error.code}, Message: ${error.message}`);
    }
  }

  registerCommandCallback() {
    this.cleanupCommandCallback = this.ctx.componentCommandReceiver.registerCommandCallback(
      this.tag,
      (command: COMMAND_NAME, args: string[]) => this.webViewBaseOperate?.registerCommandCallback(command, args));
  }

  onLoadingStart() {
    this.webViewBaseOperate?.emitLoadingStart({ progress: this.progress })
  }

  onProgressChange(event: OnProgressChangeEvent) {
    this.progress = event.newProgress
    // 修复单页面应用切换路由时 onPageBegin 未触发的问题
    if (this.controller.getUrl() !== this.url && this.progress < ONE_HUNDRED && this.allowPageStartInProgress) {
      this.allowPageStartInProgress = false;
      this.onLoadingStart();
    }
    this.webViewBaseOperate?.emitProgressChange({ progress: this.progress })
    if (this.progress === ONE_HUNDRED) {
      this.allowPageStartInProgress = true;
    }
  }

  onLoadingFinish() {
    this.webViewBaseOperate?.emitLoadingFinish({ progress: this.progress })
    this.isRefreshing = false
  }

  runInjectedJavaScript() {
    let injectedJS = this.descriptorWrapper.rawProps.injectedJavaScript
    if (this.javaScriptEnable && injectedJS != "" && this.controllerAttached) {
      try {
        this.controller.runJavaScript("(function() {\n" + injectedJS + ";\n})();")
          .then((result) => {
            Logger.debug(TAG, '[RNOH] result: ' + result);
          })
          .catch((error: string | Error) => {
            Logger.error(TAG, "[RNOH] error: " + error);
          })
      } catch (error) {
        Logger.error(TAG, `[RNOH]Errorcode: ${error.code}, Message: ${error.message}`);
      }
    }
  }

  getJsDialogTitle() {
    const url = this.controller.getUrl()
    // 对于 data: URL，只显示 'JavaScript'，类似于 Chrome
    let title = 'JavaScript';
    const isDataUrl = (url !== null) && url.startsWith("data:");


    if (!isDataUrl) {
      try {
        const dialogUrl = OSUrl.URL.parseURL(url);
        title = `网址为”${dialogUrl.protocol}//${dialogUrl.host}“的网页显示：`;
      } catch (ex) {
        title = url;
      }
    }

    return title;
  }

  onJavascriptAlert(event?: AlertEvent) {
    if (event) {
      AlertDialog.show({
        title: this.getJsDialogTitle(),
        message: event.message,
        alignment: DialogAlignment.Center,
        secondaryButton: {
          value: this.resourceToString($r('app.string.determined')),
          action: () => event.result.handleConfirm()
        },
        cancel: () => event.result.handleCancel(),
      })
    }
    return true
  }

  onJavascriptConfirm(event?: AlertEvent) {
    if (event) {
      AlertDialog.show({
        title: this.getJsDialogTitle(),
        message: event.message,
        alignment: DialogAlignment.Center,
        secondaryButton: {
          value: this.resourceToString($r('app.string.determined')),
          action: () => event.result.handleConfirm()
        },
        cancel: () => event.result.handleCancel(),
        primaryButton: {
          value: this.resourceToString($r('app.string.cancel')),
          action: () => event.result.handleCancel()
        },
      })
    }
    return true
  }

  ignoreSilentHardwareSwitchMethods(ignoreSilentHardwareSwitch: boolean) {
    this.webViewBaseOperate?.ignoreSilentHardwareSwitchMethods(ignoreSilentHardwareSwitch)
  }

  loadHtmlData(html: string = '', url: string = '') {
    try {
      this.controller.loadData(
        html,
        "text/html",
        "UTF-8",
        url,
        " "
      );
    } catch (error) {
      Logger.error(TAG, "error:" + error)
    }
  }

  controllerAttachedInit(): void {
    this.controllerAttached = true;
    this.eventEmitter = new RNC.RNCWebView.EventEmitter(this.ctx.rnInstance, this.tag)
    this.webViewBaseOperate = new BaseOperate(this.eventEmitter, this.controller)
    this.webViewBaseOperate.setCustomUserAgent(this.descriptorWrapper.rawProps.userAgent,
      this.descriptorWrapper.rawProps.applicationNameForUserAgent)
    this.webViewBaseOperate.setFraudulentWebsiteWarningEnabled(this.descriptorWrapper.rawProps.fraudulentWebsiteWarningEnabled)
    let baseUrl = this.source.baseUrl
    let uri = this.source.uri
    if (this.source.html != undefined && this.source.html != "") {
      this.loadHtmlData(this.source.html, baseUrl)
    } else if (uri != undefined && uri != "") {
      this.controller.loadUrl(uri, this.headers);
    } else {
      this.loadHtmlData()
    }
    if (!this.hasRegisterJavaScriptProxy) {
      this.registerPostMessage()
    }
  }

  onPageBegin() {
    try {
      if (this.controller.getUrl() === this.url) {
        this.onLoadingStart();
      }
      this.controller.setScrollable(this.scrollEnabled)
      this.ignoreSilentHardwareSwitchMethods(this.descriptorWrapper.rawProps.ignoreSilentHardwareSwitch)
    } catch (error) {
      Logger.debug(TAG,
        `[RNOH] setignoreSilentHardwareSwitch and setScrollable ErrorCode: ${error.code},  Message: ${error.message}, userAgent: ${this.descriptorWrapper.rawProps.userAgent}`);
    }
  }

  getBundleName() {
    let bundleFlags =
      bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_HAP_MODULE | bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_ABILITY |
      bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION;

    try {
      bundleManager.getBundleInfoForSelf(bundleFlags, (error, data) => {
        if (error) {
          Logger.error(TAG,
            `[RNOH] getBundleInfoForSelf failed ErrorCode: ${error.code},  Message: ${error.message}`);
        } else {
          this.bundleName = JSON.stringify(data.name)
        }
      });
    } catch (error) {
      Logger.error(TAG,
        `[RNOH] getBundleInfoForSelf failed ErrorCode: ${error.code},  Message: ${error.message}`);
    }
  }

  resourceToString(resource: Resource): string {
    return getContext(this).resourceManager.getStringSync(resource)
  }

  onPageEnd() {
    this.runInjectedJavaScript()
    this.onLoadingFinish()
  }

  getPermissionDialogMessage(event: OnPermissionRequestEvent) {
    let permissionDialogMessage = ''
    permissionDialogMessage = event.request.getAccessibleResource().toString()
    switch (permissionDialogMessage) {
      case ProtectedResourceType.VIDEO_CAPTURE:
        permissionDialogMessage = this.resourceToString($r('app.string.use_your_camera'))
        break;
      case ProtectedResourceType.AUDIO_CAPTURE:
        permissionDialogMessage = this.resourceToString($r('app.string.use_your_microphone'))
        break;
      case ProtectedResourceType.MidiSysex:
        permissionDialogMessage = this.resourceToString($r('app.string.midi_sysex_resources'))
        break;
      case ProtectedResourceType.VIDEO_CAPTURE + "," + ProtectedResourceType.AUDIO_CAPTURE:
        permissionDialogMessage = this.resourceToString($r('app.string.use_your_camera_and_microphone'))
        break;
    }
    return permissionDialogMessage;
  }

  onLoadIntercept(event: OnLoadInterceptEvent): boolean {
    Logger.debug(TAG, `onLoadIntercept start`)
    this.webViewBaseOperate?.setLockIdentifier(BaseOperate.generateLockIdentifier())

    const params = new ShouldStartParams();
    params.lockIdentifier = this.webViewBaseOperate?.getLockIdentifier() || -1
    this.ctx.runOnWorkerThread(new WorkerRunnable(), params);
    this.webViewBaseOperate?.emitShouldStartLoadWithRequest(event)
    const startTime = Date.now();
    // 当lockState没变化并且小于超时时间，阻塞进程
    while (params.lockState === ShouldOverrideCallbackState.UNDECIDED &&
      Date.now() - startTime < RNCWebView.SHOULD_OVERRIDE_URL_LOADING_TIMEOUT) {
      // 空循环等待
    }

    Logger.debug('params.lockState res:' + params.lockState + `, wait time is ${Date.now() - startTime}`);
    return params.lockState === ShouldOverrideCallbackState.SHOULD_OVERRIDE;
  }

  onOverrideUrlLoading(event: WebResourceRequest) {
    this.webViewBaseOperate?.emitShouldStartLoadWithRequestOverrideUrlLoading(event)
    return false
  }

  build() {
    Stack() {
      Web({ src: "", controller: this.controller, renderMode: this.renderMode })
        .mixedMode(this.mode)
        .onPermissionRequest((event: OnPermissionRequestEvent) => {
          if (event && (this.getPermissionDialogMessage(event) != "TYPE_SENSOR")) {
            AlertDialog.show({
              title: this.resourceToString($r('app.string.on_confirm')) + event.request.getOrigin(),
              message: this.getPermissionDialogMessage(event),
              alignment: DialogAlignment.Center,
              primaryButton: {
                value: $r('app.string.deny'),
                action: () => {
                  event.request.deny();
                }
              },
              secondaryButton: {
                value: $r('app.string.on_confirm'),
                action: () => {
                  event.request.grant(event.request.getAccessibleResource());
                }
              },
              cancel: () => {
                event.request.deny();
              }
            })
          }
        })

        .width(this.webviewWidth)
        .height(this.webviewHeight)
        .fileAccess(this.allowFileAccess)
        .constraintSize({ minHeight: MINHEIGHT })
        .overScrollMode(this.overScrollMode)
        .javaScriptAccess(this.javaScriptEnable)
        .javaScriptOnDocumentStart(this.injectedJavaScriptBeforeContentLoaded)
        .horizontalScrollBarAccess(this.descriptorWrapper.rawProps.showsHorizontalScrollIndicator)
        .verticalScrollBarAccess(this.descriptorWrapper.rawProps.showsVerticalScrollIndicator)
        .overviewModeAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
        .textZoomRatio(this.descriptorWrapper.rawProps.textZoom)
        .backgroundColor(BaseOperate.getColorMode(this.ctx.uiAbilityContext,
          this.descriptorWrapper.rawProps.forceDarkOn) === WebDarkMode.On ? Color.White : Color.Transparent)
        .darkMode(BaseOperate.getColorMode(this.ctx.uiAbilityContext, this.descriptorWrapper.rawProps.forceDarkOn))
        .forceDarkAccess(this.forceDark)
        .cacheMode(this.cacheMode)
        .minFontSize(this.minFontSize)
        .domStorageAccess(this.descriptorWrapper.rawProps.domStorageEnabled)
        .zoomAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
        .overScrollMode(this.overScrollMode)
        .onProgressChange((event: OnProgressChangeEvent) => this.onProgressChange(event))
        .onScroll((event: OnScrollEvent) => this.webViewBaseOperate?.emitScroll(event))
        .nestedScroll({
          scrollForward: this.nestedScroll,
          scrollBackward: this.nestedScroll,
        })
        .onPageBegin(() => this.onPageBegin())
        .onPageEnd(() => this.onPageEnd())
        .onErrorReceive((event: OnErrorReceiveEvent) => this.webViewBaseOperate?.emitLoadingError(event))
        .onHttpErrorReceive((event: OnHttpErrorReceiveEvent) => this.webViewBaseOperate?.emitHttpError(event))
        .onControllerAttached(() => this.controllerAttachedInit())
        .onAlert((event: AlertEvent) => this.onJavascriptAlert(event))
        .onConfirm((event: AlertEvent) => this.onJavascriptConfirm(event))
        .onDownloadStart((event: OnDownloadStartEvent) => this.webViewBaseOperate?.onDownloadStart(event))
        .geolocationAccess(this.descriptorWrapper.rawProps.geolocationEnabled)
        .onGeolocationShow((event) => {
          if (event && (this.descriptorWrapper.rawProps.geolocationEnabled != undefined)) {
            event.geolocation.invoke(event.origin, this.descriptorWrapper.rawProps.geolocationEnabled, true);
          }
        })
        .mediaPlayGestureAccess(this.descriptorWrapper.rawProps.mediaPlaybackRequiresUserAction)
        .onRenderExited((event: OnRenderExitedEvent) => this.webViewBaseOperate?.onRenderExited(event))
        .onLoadIntercept((event: OnLoadInterceptEvent) => this.onLoadIntercept(event))
        .onTitleReceive((event: OnTitleReceiveEvent) => this.webViewBaseOperate?.onTitleReceive(event))
    }
    .width(this.webviewWidth)
    .height(this.webviewHeight)
    .position({
      x: this.descriptorWrapper.layoutMetrics.frame.origin.x,
      y: this.descriptorWrapper.layoutMetrics.frame.origin.y
    })
  }

  private onDescriptorWrapperChange(descriptorWrapper: WebViewDescriptor) {
    this.initVariable()
    this.descriptorWrapper = descriptorWrapper
    if (this.source.html != "" && this.html != this.source.html) {
      if (this.controllerAttached) {
        this.loadHtmlData(this.source.html, this.source.baseUrl)
        Logger.debug(TAG, "[RNOH] html is update")
        this.html = this.source.html
      }
    } else if (this.url != this.source.uri) {
      if (this.source.uri != "") {
        Logger.debug(TAG, `[RNOH] newDescriptor props update uri: ` + this.source.uri);
        if (this.controllerAttached) {
          this.controller.loadUrl(this.descriptorWrapper.rawProps.newSource.uri, this.headers)
        }
      } else {
        this.loadHtmlData()
      }
      this.url = this.source.uri as string;
    }
    if (this.controllerAttached) {
      this.controller?.setScrollable(this.descriptorWrapper.rawProps.scrollEnabled)
    }
  }

  private initVariable() {
    this.descriptorWrapper = this.ctx.descriptorRegistry.getDescriptor<WebViewDescriptor>(this.tag)
    this.javaScriptEnable = this.descriptorWrapper.rawProps.javaScriptEnabled;
    this.minFontSize =
      this.descriptorWrapper.rawProps.minimumFontSize ? this.descriptorWrapper.rawProps.minimumFontSize : EIGHT;
    this.cacheMode =
      this.descriptorWrapper.rawProps.cacheEnabled ?
        this.webViewBaseOperate?.transCacheMode(this.descriptorWrapper.rawProps.cacheMode as CACHE_MODE) as number :
      CacheMode.Online;
    this.source = {
      uri: this.descriptorWrapper.rawProps.newSource.uri ?? "",
      method: this.descriptorWrapper.rawProps.newSource.method ?? "",
      body: this.descriptorWrapper.rawProps.newSource.body ?? "",
      html: this.descriptorWrapper.rawProps.newSource.html ?? "",
      baseUrl: this.descriptorWrapper.rawProps.newSource.baseUrl ?? "",
      headers: this.descriptorWrapper.rawProps.newSource.headers ?? []
    }
    if (this.source.headers) {
      this.source.headers.forEach(item => {
        if (item.name && item.value) {
          this.headers.push({ headerKey: item.name, headerValue: item.value })
        }
      })
    }
    // this.html = this.source.html
    this.webviewWidth = this.descriptorWrapper.layoutMetrics.frame.size.width
    this.webviewHeight = this.descriptorWrapper.layoutMetrics.frame.size.height
    this.scrollEnabled = this.descriptorWrapper.rawProps.scrollEnabled;
    // 当禁止滚动时，使用父组件滚动
    this.nestedScroll = this.scrollEnabled ? NestedScrollMode.SELF_FIRST : NestedScrollMode.PARENT_FIRST;
    if (this.source.uri && this.source.uri.toString().startsWith("asset://")) {
      this.source.uri = $rawfile(this.source.uri.toString().replace("asset://", this.ctx.rnInstance.getAssetsDest()));
    }

    if (this.descriptorWrapper.rawProps.overScrollMode === WebViewOverScrollMode.ALWAYS) {
      this.overScrollMode = OverScrollMode.ALWAYS
    } else if (this.descriptorWrapper.rawProps.overScrollMode === WebViewOverScrollMode.NEVER) {
      this.overScrollMode = OverScrollMode.NEVER
    }

    this.overScrollMode = this.descriptorWrapper.rawProps.bounces ? OverScrollMode.ALWAYS : OverScrollMode.NEVER;

    if (this.descriptorWrapper.rawProps.injectedJavaScriptBeforeContentLoaded) {
      this.injectedJavaScriptBeforeContentLoaded = [
        { script: this.descriptorWrapper.rawProps.injectedJavaScriptBeforeContentLoaded, scriptRules: ["*"] }
      ];
    }
  }

  private registerPostMessage() {
    if (this.messagingEnabled == this.descriptorWrapper.rawProps.messagingEnabled) {
      return;
    }
    this.messagingEnabled = this.descriptorWrapper.rawProps.messagingEnabled;
    if (this.messagingEnabled) {
      let bridge: RNCWebViewBridge = {
        postMessage: (data: string) => {
          Logger.debug(TAG, `[RNOH] bridge postMessage,  ${JSON.stringify(data)}`);
          if (this.controller != null) {
            let result: WebViewEventParams = this.createWebViewEvent("onMessage")
            if (result) {
              result.data = data.toString()
              result.lockIdentifier = this.webViewBaseOperate?.getLockIdentifier()
              this.eventEmitter!.emit("message", result as ResultType);
            }
          }
        }
      };
      this.controller.registerJavaScriptProxy(bridge, JAVASCRIPT_INTERFACE, ["postMessage"])
      this.source.uri ?
      this.controller.loadUrl(this.source.uri, this.headers) : this.controller.refresh()
      this.hasRegisterJavaScriptProxy = true
    }
  }
}