import { Jsonify } from "type-fest";

import { UserId } from "../../types/guid";
import { DerivedStateDependencies, StorageKey } from "../../types/state";

import { KeyDefinition } from "./key-definition";
import { StateDefinition } from "./state-definition";
import { UserKeyDefinition } from "./user-key-definition";

declare const depShapeMarker: unique symbol;
/**
 * A set of options for customizing the behavior of a {@link DeriveDefinition}
 */
type DeriveDefinitionOptions<TFrom, TTo, TDeps extends DerivedStateDependencies = never> = {
  /**
   * A function to use to convert values from TFrom to TTo. This is called on each emit of the parent state observable
   * and the resulting value will be emitted from the derived state observable.
   *
   * @param from Populated with the latest emission from the parent state observable.
   * @param deps Populated with the dependencies passed into the constructor of the derived state.
   * These are constant for the lifetime of the derived state.
   * @returns  The derived state value or a Promise that resolves to the derived state value.
   */
  derive: (from: TFrom, deps: TDeps) => TTo | Promise<TTo>;
  /**
   * A function to use to safely convert your type from json to your expected type.
   *
   * **Important:** Your data may be serialized/deserialized at any time and this
   *  callback needs to be able to faithfully re-initialize from the JSON object representation of your type.
   *
   * @param jsonValue The JSON object representation of your state.
   * @returns The fully typed version of your state.
   */
  deserializer: (serialized: Jsonify<TTo>) => TTo;
  /**
   * An object defining the dependencies of the derive function. The keys of the object are the names of the dependencies
   * and the values are the types of the dependencies.
   *
   * for example:
   * ```
   * {
   *   myService: MyService,
   *   myOtherService: MyOtherService,
   * }
   * ```
   */
  [depShapeMarker]?: TDeps;
  /**
   * The number of milliseconds to wait before cleaning up the state after the last subscriber has unsubscribed.
   * Defaults to 1000ms.
   */
  cleanupDelayMs?: number;
  /**
   * Whether or not to clear the derived state when cleanup occurs. Defaults to true.
   */
  clearOnCleanup?: boolean;
};

/**
 * DeriveDefinitions describe state derived from another observable, the value type of which is given by `TFrom`.
 *
 * The StateDefinition is used to describe the domain of the state, and the DeriveDefinition
 * sub-divides that domain into specific keys. These keys are used to cache data in memory and enables derived state to
 * be calculated once regardless of multiple execution contexts.
 */

export class DeriveDefinition<TFrom, TTo, TDeps extends DerivedStateDependencies> {
  /**
   * Creates a new instance of a DeriveDefinition. Derived state is always stored in memory, so the storage location
   * defined in @link{StateDefinition} is ignored.
   *
   * @param stateDefinition The state definition for which this key belongs to.
   * @param uniqueDerivationName The name of the key, this should be unique per domain.
   * @param options A set of options to customize the behavior of {@link DeriveDefinition}.
   * @param options.derive A function to use to convert values from TFrom to TTo. This is called on each emit of the parent state observable
   * and the resulting value will be emitted from the derived state observable.
   * @param options.cleanupDelayMs The number of milliseconds to wait before cleaning up the state after the last subscriber has unsubscribed.
   * Defaults to 1000ms.
   * @param options.dependencyShape An object defining the dependencies of the derive function. The keys of the object are the names of the dependencies
   * and the values are the types of the dependencies.
   * for example:
   * ```
   * {
   *   myService: MyService,
   *   myOtherService: MyOtherService,
   * }
   * ```
   *
   * @param options.deserializer A function to use to safely convert your type from json to your expected type.
   *   Your data may be serialized/deserialized at any time and this needs callback needs to be able to faithfully re-initialize
   *   from the JSON object representation of your type.
   */
  constructor(
    readonly stateDefinition: StateDefinition,
    readonly uniqueDerivationName: string,
    readonly options: DeriveDefinitionOptions<TFrom, TTo, TDeps>,
  ) {}

  /**
   * Factory that produces a {@link DeriveDefinition} from a {@link KeyDefinition} or {@link DeriveDefinition} and new name.
   *
   * If a `KeyDefinition` is passed in, the returned definition will have the same key as the given key definition, but
   * will not collide with it in storage, even if they both reside in memory.
   *
   * If a `DeriveDefinition` is passed in, the returned definition will instead use the name given in the second position
   * of the tuple. It is up to you to ensure this is unique within the domain of derived state.
   *
   * @param options A set of options to customize the behavior of {@link DeriveDefinition}.
   * @param options.derive A function to use to convert values from TFrom to TTo. This is called on each emit of the parent state observable
   * and the resulting value will be emitted from the derived state observable.
   * @param options.cleanupDelayMs The number of milliseconds to wait before cleaning up the state after the last subscriber has unsubscribed.
   * Defaults to 1000ms.
   * @param options.dependencyShape An object defining the dependencies of the derive function. The keys of the object are the names of the dependencies
   * and the values are the types of the dependencies.
   * for example:
   * ```
   * {
   *   myService: MyService,
   *   myOtherService: MyOtherService,
   * }
   * ```
   *
   * @param options.deserializer A function to use to safely convert your type from json to your expected type.
   *   Your data may be serialized/deserialized at any time and this needs callback needs to be able to faithfully re-initialize
   *   from the JSON object representation of your type.
   * @param definition
   * @param options
   * @returns
   */
  static from<TFrom, TTo, TDeps extends DerivedStateDependencies = never>(
    definition:
      | KeyDefinition<TFrom>
      | UserKeyDefinition<TFrom>
      | [DeriveDefinition<unknown, TFrom, DerivedStateDependencies>, string],
    options: DeriveDefinitionOptions<TFrom, TTo, TDeps>,
  ) {
    if (isFromDeriveDefinition(definition)) {
      return new DeriveDefinition(definition[0].stateDefinition, definition[1], options);
    } else {
      return new DeriveDefinition(definition.stateDefinition, definition.key, options);
    }
  }

  static fromWithUserId<TKeyDef, TTo, TDeps extends DerivedStateDependencies = never>(
    definition:
      | KeyDefinition<TKeyDef>
      | UserKeyDefinition<TKeyDef>
      | [DeriveDefinition<unknown, TKeyDef, DerivedStateDependencies>, string],
    options: DeriveDefinitionOptions<[UserId, TKeyDef], TTo, TDeps>,
  ) {
    if (isFromDeriveDefinition(definition)) {
      return new DeriveDefinition(definition[0].stateDefinition, definition[1], options);
    } else {
      return new DeriveDefinition(definition.stateDefinition, definition.key, options);
    }
  }

  get derive() {
    return this.options.derive;
  }

  deserialize(serialized: Jsonify<TTo>): TTo {
    return this.options.deserializer(serialized);
  }

  get cleanupDelayMs() {
    return this.options.cleanupDelayMs < 0 ? 0 : this.options.cleanupDelayMs ?? 1000;
  }

  get clearOnCleanup() {
    return this.options.clearOnCleanup ?? true;
  }

  buildCacheKey(location: string): string {
    return `derived_${location}_${this.stateDefinition.name}_${this.uniqueDerivationName}`;
  }

  /**
   * Creates a {@link StorageKey} that points to the data for the given derived definition.
   * @returns A key that is ready to be used in a storage service to get data.
   */
  get storageKey(): StorageKey {
    return `derived_${this.stateDefinition.name}_${this.uniqueDerivationName}` as StorageKey;
  }
}

function isFromDeriveDefinition(
  definition:
    | KeyDefinition<unknown>
    | UserKeyDefinition<unknown>
    | [DeriveDefinition<unknown, unknown, DerivedStateDependencies>, string],
): definition is [DeriveDefinition<unknown, unknown, DerivedStateDependencies>, string] {
  return Array.isArray(definition);
}
