// Type definitions for panel
// Project: panel
// Definitions by: Mixpanel (https://mixpanel.com)
import {VNode} from 'snabbdom';
import WebComponent from 'webcomponent';

export {h} from 'snabbdom';
export {jsx} from 'snabbdom-jsx-lite';

import {JsxVNode, JsxVNodeProps} from 'snabbdom-jsx-lite';

export class StateStore<State> {
  constructor(options: {store?: StateStore<State>});

  /** A readonly version of controller's state */
  get state(): State;

  /** Update the state by passing in a property bag */
  update(props?: Partial<State>): void;

  /**
   * @internal Subscribe to state updates via a listener callback.
   * Only use for rendering and debugging purposes
   */
  subscribeUpdates(listener: (props: Partial<State>) => void): void;

  /** @internal Unsubscribe the listener callback that was passed to subscribeUpdates */
  unsubscribeUpdates(listener: (props: Partial<State>) => void): void;
}

export class StateController<State> {
  constructor(options: {store?: StateStore<State>});

  /** A readonly version of controller's state */
  get state(): State;

  /** An initial default property bag for the controller's state */
  get defaultState(): State;

  /** Update the state by passing in a property bag */
  _update(props?: Partial<State>): void;

  /**
   * @internal Subscribe to state updates via a listener callback.
   * panel component uses this to trigger dom update pipeline
   * Only use for rendering and debugging purposes
   */
  subscribeUpdates(listener: (props: Partial<State>) => void): void;

  /** @internal Unsubscribe the listener callback that was passed to subscribeUpdates */
  unsubscribeUpdates(listener: (props: Partial<State>) => void): void;
}

export interface PanelHelpers {
  [helper: string]: any;
}

export interface PanelHooks<State, Params> {
  /** Function called before an update is applied */
  preUpdate?: (stateUpdate: Partial<State>, params?: Partial<Params>) => void;

  /** Function called after an update is applied */
  postUpdate?: (stateUpdate: Partial<State>, params?: Partial<Params>) => void;

  [hookName: string]: (params: any) => void;
}

// this type is not checked in the Component ContextRegistryT, the Component JS manually checks for these properties instead
export interface PanelLifecycleContext {
  // optional callback that executes each time a component using this context is connected to the DOM
  bindToComponent?(component: Component<any>): void;
  // optional callback that executes each time a component using this context is disconnected from the DOM
  unbindFromComponent?(component: Component<any>): void;
}

export interface ConfigOptions<StateT, AppStateT = unknown, ContextRegistryT = unknown, ParamT = unknown> {
  /** Function transforming state object to virtual dom tree */
  template(scope?: StateT): VNode;

  params?: {[param in keyof ParamT]: InferType<ParamT[param]> | ParamType<ParamT[param]>};

  /** Component-specific Shadow DOM stylesheet */
  css?: string;

  /** object to provide default value for params */
  defaultParams?: Partial<ParamT>;

  /** An initial default value for the component's state property */
  defaultState?: StateT;

  /** Default contexts for the component and its descendants to use if no context parent provides them */
  defaultContexts?: Partial<ContextRegistryT>;

  /** Names of contexts for the component to attach and depend upon */
  contexts?: Array<keyof ContextRegistryT>;

  /**
   * A state object to share with nested descendant components. If not set, root component
   * shares entire state object with all descendants. Only applicable to app root components.
   */
  appState?: AppStateT;

  /** Properties and functions injected automatically into template state object */
  helpers?: PanelHelpers;

  /** Extra rendering/lifecycle callbacks */
  hooks?: PanelHooks<StateT, ParamT>;

  /** Object mapping string route expressions to handler functions */
  routes?: {[route: string]: Function};

  /** Whether to apply updates to DOM immediately, instead of batching to one update per frame */
  updateSync?: boolean;

  /** Whether to use Shadow DOM */
  useShadowDom?: boolean;

  /** Defines the threshold at which 'slowRender' events will be dispatched, defaults to 20ms */
  slowThreshold?: number;
}

export interface AttrSchema {
  /** Type of the attribute. One of 'string' | 'number' | 'boolean' | 'json' */
  type: string;

  /** Default value if the attr is not defined */
  default?: any;

  /** Description of attribute, what it does e.t.c */
  description?: string;

  /** Possible values of an attribute. e.g ['primary', 'secondary'] */
  enum?: string[];

  /** When setAttribute is invoked, console.warn that attr is deprecated e.g 'use xyz instead' */
  deprecatedMsg?: string;

  /**
   * For a type: `json` attr, the typescript type that corresponds to it.
   * Can be used to auto-generate Attrs interface
   */
  tsType?: string;

  /**
   * Explicitly require an attribute to be passed, useful when no default value can be inferred.
   */
  required?: boolean;
}

export type AttrsSchema<T> = {
  [attr in keyof T]: string | AttrSchema;
};

export interface AnyAttrs {
  [attr: string]: any;
}

type InferType<T> = T extends string
  ? StringConstructor
  : T extends number
  ? NumberConstructor
  : T extends boolean
  ? BooleanConstructor
  : T extends unknown[]
  ? ArrayConstructor
  : T extends Map<unknown, unknown>
  ? MapConstructor
  : T extends Set<unknown>
  ? SetConstructor
  : T extends (...args: any[]) => any
  ? FunctionConstructor
  : ObjectConstructor;

interface ParamType<T> {
  type: InferType<T>;
  required?: boolean;
}

export class Component<
  StateT,
  AttrsT = AnyAttrs,
  AppStateT = unknown,
  AppT = unknown,
  ContextRegistryT = unknown,
  ParamT extends Record<string, any> = unknown
> extends WebComponent {
  /** The first Panel Component ancestor in the DOM tree; null if this component is the root */
  $panelParent: Component<unknown>;

  /**
   * Attributes schema that defines the component's html attributes and their types
   * Panel auto parses attribute changes into this.attrs object and $attrs template helper
   */
  static get attrsSchema(): {[attr: string]: string | AttrSchema};

  /** New panel params */
  params: Readonly<ParamT>;

  /** A reference to the top-level component */
  app: AppT;

  /** State object to share with nested descendant components */
  appState: AppStateT;

  /** Refers to the outer-most element in the template file for shadow DOM components. Otherwise, el refers to the component itself. */
  el: HTMLElement;

  /** A flag that represents whether the component is currently connected and initialized */
  initialized: boolean;

  /** Defines the state of the component, including all the properties required for rendering */
  state: StateT;

  readonly timings: Readonly<{
    /** The time in ms that the component constructor ran */
    createdAt: number;
    /** The time in ms that component initialization started (also see 'initializingCompletedAt') */
    initializingStartedAt: number;
    /** The time in ms that component initialization completed (also see 'initializingStartedAt') */
    initializingCompletedAt: number;
    /** The time in ms that the last #attributeChangedCallback ran */
    lastAttributeChangedAt: number;
    /** The time in ms that the last #update ran */
    lastUpdateAt: number;
    /** The time in ms that the last render ran */
    lastRenderAt: number;
  }>;

  /** Applies the static stylesheet for this component class */
  applyStaticStyle(styleSheetText: null | string, options?: {ignoreCache: boolean}): void;

  /** Defines standard component configuration */
  get config(): ConfigOptions<StateT, AppStateT, ContextRegistryT, ParamT>;

  /**
   * Template helper functions defined in config object, and exposed to template code as $helpers.
   * This getter uses the component's internal config cache.
   */
  get helpers(): this['config']['helpers'];

  /** Gets the attribute value. Throws an error if attr not defined in attrsSchema */
  attr<A extends keyof AttrsT>(attr: A): AttrsT[A];

  /** Attributes parsed from component's html attributes using attrsSchema */
  attrs(): AttrsT;

  /**
   * For use inside view templates, to create a child Panel component nested under this
   * component, which will share its state object and update cycle.
   */
  child<T = object>(tagName: string, config?: T): VNode;

  /**
   * Searches the component's Panel ancestors for the first component of the
   * given type (HTML tag name).
   */
  findPanelParentByTagName(tagName: string): Component<any>;

  /**
   * Fetches a value from the component's configuration map (a combination of
   * values supplied in the config() getter and defaults applied automatically).
   */
  getConfig<K extends keyof ConfigOptions<StateT, AppStateT, ContextRegistryT, ParamT>>(key: K): this['config'][K];

  /** Sets a value in the component's configuration map after element initialization */
  setConfig<K extends keyof ConfigOptions<StateT, AppStateT, ContextRegistryT, ParamT>>(
    key: K,
    val: ConfigOptions<StateT, AppStateT, ContextRegistryT>[K],
  ): void;

  /**
   * set the params for the this component
   * triggers a component update
   * if shouldComponentUpdate callback returns true
   */
  setParams(params: Partial<ParamT>): void;

  /**
   * Same API as react's `shouldComponentUpdate` usage
   * if child component implements this method, parent implmentation wil be discarded
   * only difference is the `params` or `state` could sometimes be null indicating that
   * the update is not related to `params` or `state`
   *
   * To be overridden by subclasses, defining conditional logic for whether
   * a component should rerender its template given the state and params to be applied.
   * In most cases this method can be left untouched, but can provide improved
   * performance when dealing with very many DOM elements.
   *
   * @example
   * shouldComponentUpdate(params, state) {
   *   // don't need to rerender if result set ID hasn't changed
   *   if (state && state.largeResultSetID === this._cachedResultID) {
   *      return false
   *   }
   *   if (params && params.bookmark.id === this.params.bookmark.id) {
   *     return false;
   *   }
   *   return !shallowEqual(params, this.params);
   * }
   */
  shouldComponentUpdate(params: ParamT | null, state: StateT | null): boolean;

  /**
   * To be overridden by subclasses, defining conditional logic for whether
   * a component should rerender its template given the state and params to be applied.
   * In most cases this method can be left untouched, but can provide improved
   * performance when dealing with very many DOM elements.
   *
   * @deprecated use shouldComponentUpdate instead
   */
  shouldUpdate(state: StateT): boolean;

  /**
   * Executes the route handler matching the given URL fragment, and updates
   * the URL, as though the user had navigated explicitly to that address.
   */
  navigate(fragment: string, stateUpdate?: Partial<StateT>): void;

  /** Run a user-defined hook with the given parameters */
  runHook: (
    hookName: keyof ConfigOptions<StateT, AppStateT, ContextRegistryT>['hooks'],
    options: {cascade: boolean; exclude: Component<any, any>},
    params: any,
  ) => void;

  /**
   * Applies a state update specifically to app state shared across components.
   * In apps which don't specify `appState` in the root component config, all
   * state is shared across all parent and child components and the standard
   * update() method should be used instead.
   */
  updateApp(stateUpdate?: Partial<AppStateT>): void;

  /**
   * Applies a state update, triggering a re-render check of the component as
   * well as any other components sharing the same state. This is the primary
   * means of updating the DOM in a Panel application.
   */
  update(stateUpdate?: Partial<StateT> | ((state: StateT) => Partial<StateT>)): void;

  /**
   * Helper function which will queue a function to be run once the component has been
   * initialized and added to the DOM. If the component has already had its connectedCallback
   * run, the function will run immediately.
   *
   * It can optionally return a function to be enqueued to be run just before the component is
   * removed from the DOM. This occurs during the disconnectedCallback lifecycle.
   */
  onConnected(callback: () => void | (() => void)): void;

  /**
   * Helper function which will queue a function to be run just before the component is
   * removed from the DOM. This occurs during the disconnectedCallback lifecycle.
   */
  onDisconnected(callback: () => void): void;

  /**
   * Returns the default context of the highest (ie. closest to the document root) ancestor component
   * that has configured a default context for the context name,
   * or it will return the component's own default context if no ancestor context was found.
   */
  getContext<ContextKey extends keyof ContextRegistryT>(contextName: ContextKey): ContextRegistryT[ContextKey];
}

/**
 * Panel component that only accepts 3 generic types
 */
export class ParamComponent<ParamT = unknown, StateT = unknown, ContextRegistryT = unknown> extends Component<
  StateT,
  unknown,
  unknown,
  unknown,
  ContextRegistryT,
  ParamT
> {}

// define jsx IntrinsicElement inside namespace jsx to play well with react
declare global {
  /**
   * opt-in jsx intrinsic global interfaces
   * see: https://www.typescriptlang.org/docs/handbook/jsx.html#type-checking
   */
  namespace jsx {
    namespace JSX {
      type Element = JsxVNode;
      interface IntrinsicElements {
        [elemName: string]: JsxVNodeProps;
      }
    }
  }
}
