/* eslint-disable @typescript-eslint/no-unused-vars */
import { nothing, TemplateResult } from 'lit'
import { ActivityState, IntentResult, type Intent } from './ActivityManager.js'
import { FragmentManager } from './FragmentManager.js'
import type { Fragment } from './Fragment.js'
import type { Application, UpdateRequest } from './Application.js'
import { bound } from '../decorators/bound.js'
import type { ActivityDetail, ActivityWithResultDetail } from '../events/IntentEvents.js'
import { EventTypes } from '../events/EventTypes.js'

/**
 * `Activity` is a crucial component of your app. An application sends an intent
 * to bring the activity. If it's found, the activity gets to the foreground and start its activity.
 *
 * **Lifecycle**
 *
 * - `onCreated()` - fires when the system first creates the activity.
 * - `onStart()` - the activity becomes visible to the user as the app prepares for
 *  the activity to enter the foreground and become interactive.
 * - `onResume()` - the state in which the app interacts with the user.
 * - `onPause()` - the first indication that the user is leaving your activity.
 * - `onStop()` - your activity is no longer visible to the user.
 * - `onDestroy()`- is called before the activity is destroyed
 */
export class Activity extends EventTarget {
  /**
   * To be set by an activity to replace the default render root.
   * This is particularly useful when the activity is a modal dialog
   * and the content should be rendered outside the application's render root.
   */
  renderRoot?: HTMLElement
  /**
   * An activity can define its action name that does not change.
   * It then should be used as the activity registration key.
   * This way other activities/fragments can use it to call the activity.
   */
  static action?: string

  public state: ActivityState = ActivityState.Initialized
  protected parent: Application
  /**
   * The fragment manager that manages fragments in this activity.
   */
  protected manager: FragmentManager
  protected resultData?: unknown

  protected exitCode = IntentResult.RESULT_CANCELED

  get resultCode(): IntentResult {
    return this.exitCode
  }

  /**
   * When starting an new activity for result, we add the request code here
   * so that the manager can track which activity originally called the
   * activity for result.
   */
  protected pendingRequestCodes: number[] = []

  constructor(parent: Application) {
    super()
    this.parent = parent
    this.manager = new FragmentManager(this)
  }

  /**
   * Called when the activity is starting. This is where most initialization should go.
   * You can call `finish()` from within this function, in which case `onDestroy()` will be
   * immediately called after `onCreate()` without any of the rest of the activity lifecycle
   * (`onStart()`, `onResume()`, `onPause()`, etc) executing.
   *
   * In the `onCreate()` method, perform basic activity startup logic that happens only once
   * for the entire life of the activity.
   * @param intent Optional intent data.
   */
  onCreate(intent?: Intent): void | Promise<void> {
    this.dispatchEvent(new CustomEvent('activity:create', { detail: intent }))
  }

  /**
   * Called when the activity becomes visible.
   * As `onCreate()` exits, the activity enters the `Started` state, and the activity becomes visible
   * to the user. This callback contains what amounts to the activity’s final preparations for coming
   * to the foreground and becoming interactive.
   *
   * Called after `onCreate()`.  It will usually be followed by onResume().
   * This is a good place to begin running animations, etc.
   * You can call `finish()` from within this function, in which case `onStop()` will be
   * immediately called after `onStart()` without the lifecycle transitions
   * in-between (`onResume()`, `onPause()`, etc) executing.
   */
  onStart(): void | Promise<void> {
    this.dispatchEvent(new CustomEvent('activity:start', { detail: null }))
  }

  /**
   * Called when the activity gains focus.
   * Called after the `onPause()`. This is usually a hint for your activity to start interacting
   * with the user, which is a good indicator that the activity became active and ready to receive input.
   * This sometimes could also be a transit state toward another resting state.
   * For instance, an activity may be relaunched to `onPause()` due to configuration changes and
   * the activity was visible, but wasn't the top-most activity of an activity task.
   * The `render()` function is always called after this callback.
   */
  onResume(): void | Promise<void> {
    this.dispatchEvent(new CustomEvent('activity:resume', { detail: null }))
  }

  /**
   * Called when the activity loses focus.
   * Called as part of the activity lifecycle when the user no longer actively interacts with the activity,
   * but it is still visible on screen. The counterpart to `onResume()`.
   *
   * When activity B is launched in front of activity A, this callback will be invoked on A.
   * B will not be created until A's `onPause()` returns, so be sure to not do anything lengthy here.
   *
   * This callback is mostly used for saving any persistent state the activity is editing,
   * to present a "edit in place" model to the user and making sure nothing is lost if there are
   * not enough resources to start the new activity without first killing this one.
   * This is also a good place to stop things that consume a noticeable amount of CPU in order
   * to make the switch to the next activity as fast as possible.
   */
  onPause(): void | Promise<void> {
    this.dispatchEvent(new CustomEvent('activity:pause', { detail: null }))
  }

  /**
   * Called when you are no longer visible to the user.
   * You will next receive either onRestart(), onDestroy(), or nothing, depending on later user activity.
   * This is a good place to stop refreshing UI, running animations and other visual things.
   */
  onStop(): void | Promise<void> {
    this.dispatchEvent(new CustomEvent('activity:stop', { detail: null }))
  }

  onRestart(): void | Promise<void> {
    this.dispatchEvent(new CustomEvent('activity:restart', { detail: null }))
  }

  /**
   * Perform any final cleanup before an activity is destroyed.
   * This can happen because the activity is finishing (someone called finish() on it).
   */
  onDestroy(): void | Promise<void> {
    this.dispatchEvent(new CustomEvent('activity:destroy', { detail: null }))
    this.manager.onDestroy()
  }

  /**
   * Called when a new intent is delivered
   * @param intent New intent data.
   */
  onNewIntent(intent: Intent): void | Promise<void> {
    //
  }

  /**
   * Called by the renderer when the activity is rendered for the first time.
   */
  onFirstRender(): void {
    //
  }

  /**
   * A function called when it should render the view.
   */
  render(): TemplateResult | typeof nothing {
    return nothing
  }

  /**
   * Finishes the activity.
   */
  async finish(): Promise<void> {
    await this.getApplication().manager.finishActivity(this)
  }

  /**
   * When the activity changes its state it should call the
   * `requestUpdate()` method to inform the parent application to
   * perform the render operation.
   * The function dispatches the `activity:update` custom event
   * that is handled by the parent application.
   *
   * Note, the application may choose to not call the render procedure
   * (for example, when the activity is not visible). The rendering
   * function is asynchronous. The only way to know the activity is being rendered,
   * is when the `render()` method if called.
   */
  requestUpdate(opts: UpdateRequest = {}): void {
    const { activity = true } = opts
    if (activity) {
      this.manager.updateActiveFragments()
    }
    this.parent.requestUpdate(opts)
  }

  /**
   * Registers a new fragment.
   * @param key The name of the fragment
   * @param fragment The fragment to register.
   */
  async addFragment(key: string, fragment: Fragment, data?: unknown): Promise<void> {
    await this.manager.attachFragment(key, fragment, this, data)
  }

  async showFragment(key: string, root?: HTMLElement): Promise<void> {
    await this.manager.showFragment(key, root)
  }

  async hideFragment(key: string): Promise<void> {
    const fragment = this.manager.findFragment(key)
    if (fragment) {
      await this.manager.hideFragment(fragment)
    }
  }

  /**
   * Unifies Fragment and Activity interfaces.
   * @returns `this`
   */
  getApplication(): Application {
    return this.parent
  }

  /**
   * A shortcut to the `manager` function.
   * @param intent The intent to start
   */
  startActivity(intent: Intent): Promise<void> {
    return this.parent.manager.startActivity(intent)
  }

  /**
   * Starts a new activity for a result.
   * @param intent The intent that created this activity.
   * @returns The request code created with this call.
   */
  async startActivityForResult(intent: Intent): Promise<number> {
    const { manager } = this.getApplication()
    const code = manager.createRequestCode()
    this.pendingRequestCodes.push(code)
    await manager.startActivityForResult(intent, code)
    return code
  }

  /**
   * Call this method to return data to the activity that started this activity.
   * @param data The data to return.
   */
  setResult(resultCode: IntentResult, data?: unknown): void {
    this.exitCode = resultCode
    this.resultData = data
  }

  /**
   * Called when an activity you launched exits, giving you the `requestCode` you started it with,
   * the `resultCode` it returned, and any additional data from it. The `resultCode` will be `RESULT_CANCELED`
   * if the activity explicitly returned that, didn't return any result, or crashed during its operation.
   *
   * An activity can never receive a result in the resumed state. You can count on `onResume()` being called
   * after this method, though not necessarily immediately after. If the activity was resumed,
   * it will be paused and the result will be delivered, followed by `onResume()`.
   *
   * @param requestCode The request code passed to startActivityForResult.
   * @param data The result from the activity.
   * @param intent The intent that was used to start the activity.
   */
  async onActivityResult(requestCode: number, resultCode: IntentResult, intent: Intent): Promise<void> {
    const index = this.pendingRequestCodes.indexOf(requestCode)
    if (index !== -1) {
      this.pendingRequestCodes.splice(index, 1)
    }
    // First we check whether any of the fragments has the code.
    const fragment = this.manager.findByRequestCode(requestCode)
    if (fragment) {
      return fragment.onActivityResult(requestCode, resultCode, intent)
    }
    if (this.constructor === Activity) {
      // eslint-disable-next-line no-console
      console.info(
        `Activity#onActivityResult not implemented. Request code: ${requestCode}, result code: ${resultCode}`,
        intent
      )
    }
  }

  getActivity(): Activity {
    return this
  }

  /**
   * Checks whether this activity initiated the activity for result.
   * @param code The request code to look for.
   * @returns `true` if the activity has the request code.
   */
  hasRequestCode(code: number): boolean {
    return this.pendingRequestCodes.includes(code)
  }

  getResult(): unknown | undefined {
    return this.resultData
  }

  /**
   * A handler for the intent event dispatched by web components hosted by this activity.
   *
   * **Usage example:**
   *
   * ```ts
   * <custom-element @startactivity="${this.handleIntentEvent}"></custom-element>
   * <custom-element @startactivityforresult="${this.handleIntentEvent}"></custom-element>
   * ```
   *
   * @param event The event that was dispatched.
   * @returns A promise that resolves when the intent is handled.
   * @throws An error if the activity has no activity.
   * @throws An error if the event type is not recognized.
   */
  @bound
  async handleIntentEvent(event: CustomEvent<ActivityDetail | ActivityWithResultDetail>): Promise<void> {
    if (event.type === EventTypes.Intent.startActivityForResult) {
      const info = event.detail as ActivityWithResultDetail
      await this.startActivityForResult(info.intent)
    } else if (event.type === EventTypes.Intent.startActivity) {
      await this.startActivity(event.detail.intent)
    } else {
      throw new Error(`Unrecognized intent event: ${event.type}`)
    }
  }
}
