import { assertNever } from "../common/helpers-pure";
import type { MethodSignature } from "./method";

export type ModeledMethodType =
  | "none"
  | "source"
  | "sink"
  | "summary"
  | "type"
  | "neutral";

export type Provenance =
  // Generated by the dataflow model
  | "df-generated"
  // Generated by the dataflow model and manually edited
  | "df-manual"
  // Entered by the user in the editor manually
  | "manual";

export interface NoneModeledMethod extends MethodSignature {
  readonly type: "none";
}

export interface SourceModeledMethod extends MethodSignature {
  readonly type: "source";
  readonly output: string;
  readonly kind: ModeledMethodKind;
  readonly provenance: Provenance;
}

export interface SinkModeledMethod extends MethodSignature {
  readonly type: "sink";
  readonly input: string;
  readonly kind: ModeledMethodKind;
  readonly provenance: Provenance;
}

export interface SummaryModeledMethod extends MethodSignature {
  readonly type: "summary";
  readonly input: string;
  readonly output: string;
  readonly kind: ModeledMethodKind;
  readonly provenance: Provenance;
}

export interface NeutralModeledMethod extends MethodSignature {
  readonly type: "neutral";
  readonly kind: ModeledMethodKind;
  readonly provenance: Provenance;
}

export interface TypeModeledMethod extends MethodSignature {
  readonly type: "type";
  readonly relatedTypeName: string;
  readonly path: string;
}

export type ModeledMethod =
  | NoneModeledMethod
  | SourceModeledMethod
  | SinkModeledMethod
  | SummaryModeledMethod
  | NeutralModeledMethod
  | TypeModeledMethod;

export type ModeledMethodKind = string;

export function modeledMethodSupportsKind(
  modeledMethod: ModeledMethod,
): modeledMethod is
  | SourceModeledMethod
  | SinkModeledMethod
  | SummaryModeledMethod
  | NeutralModeledMethod {
  return (
    modeledMethod.type === "source" ||
    modeledMethod.type === "sink" ||
    modeledMethod.type === "summary" ||
    modeledMethod.type === "neutral"
  );
}

export function modeledMethodSupportsInput(
  modeledMethod: ModeledMethod,
): modeledMethod is SinkModeledMethod | SummaryModeledMethod {
  return modeledMethod.type === "sink" || modeledMethod.type === "summary";
}

export function modeledMethodSupportsOutput(
  modeledMethod: ModeledMethod,
): modeledMethod is SourceModeledMethod | SummaryModeledMethod {
  return modeledMethod.type === "source" || modeledMethod.type === "summary";
}

export function modeledMethodSupportsProvenance(
  modeledMethod: ModeledMethod,
): modeledMethod is
  | SourceModeledMethod
  | SinkModeledMethod
  | SummaryModeledMethod
  | NeutralModeledMethod {
  return (
    modeledMethod.type === "source" ||
    modeledMethod.type === "sink" ||
    modeledMethod.type === "summary" ||
    modeledMethod.type === "neutral"
  );
}

/**
 * Calculates the new provenance for a modeled method based on the current provenance.
 * @param modeledMethod The modeled method if there is one.
 * @returns The new provenance.
 */
export function calculateNewProvenance(
  modeledMethod: ModeledMethod | undefined,
) {
  if (!modeledMethod || !modeledMethodSupportsProvenance(modeledMethod)) {
    // If nothing has been modeled or the modeled method does not support
    // provenance, we assume that the user has entered it manually.
    return "manual";
  }

  switch (modeledMethod.provenance) {
    case "df-generated":
      // If the method has been generated and there has been a change, we assume
      // that the user has manually edited it.
      return "df-manual";
    case "df-manual":
      // If the method has had manual edits, we want the provenance to stay the same.
      return "df-manual";
    default:
      // The method has been modeled manually.
      return "manual";
  }
}

export function createModeledMethodKey(modeledMethod: ModeledMethod): string {
  const canonicalModeledMethod = canonicalizeModeledMethod(modeledMethod);
  return JSON.stringify(
    canonicalModeledMethod,
    // This ensures the keys are always in the same order
    Object.keys(canonicalModeledMethod).sort(),
  );
}

/**
 * This method will reset any properties which are not used for the specific type of modeled method.
 *
 * It will also set the `provenance` to `manual` since multiple modelings of the same method with a
 * different provenance are not actually different.
 *
 * The returned canonical modeled method should only be used for comparisons. It should not be used
 * for display purposes, saving the model, or any other purpose which requires the original modeled
 * method to be preserved.
 *
 * @param modeledMethod The modeled method to canonicalize
 */
function canonicalizeModeledMethod(
  modeledMethod: ModeledMethod,
): ModeledMethod {
  const methodSignature: MethodSignature = {
    endpointType: modeledMethod.endpointType,
    signature: modeledMethod.signature,
    packageName: modeledMethod.packageName,
    typeName: modeledMethod.typeName,
    methodName: modeledMethod.methodName,
    methodParameters: modeledMethod.methodParameters,
  };

  switch (modeledMethod.type) {
    case "none":
      return {
        ...methodSignature,
        type: "none",
      };
    case "source":
      return {
        ...methodSignature,
        type: "source",
        output: modeledMethod.output,
        kind: modeledMethod.kind,
        provenance: "manual",
      };
    case "sink":
      return {
        ...methodSignature,
        type: "sink",
        input: modeledMethod.input,
        kind: modeledMethod.kind,
        provenance: "manual",
      };
    case "summary":
      return {
        ...methodSignature,
        type: "summary",
        input: modeledMethod.input,
        output: modeledMethod.output,
        kind: modeledMethod.kind,
        provenance: "manual",
      };
    case "neutral":
      return {
        ...methodSignature,
        type: "neutral",
        kind: modeledMethod.kind,
        provenance: "manual",
      };
    case "type":
      return {
        ...methodSignature,
        type: "type",
        relatedTypeName: modeledMethod.relatedTypeName,
        path: modeledMethod.path,
      };
    default:
      assertNever(modeledMethod);
  }
}
