import type {
  HigherOrderAnimation,
  NextAnimation,
  StyleLayoutAnimation,
} from './commonTypes';
import type { ParsedColorArray } from '../Colors';
import {
  isColor,
  convertToRGBA,
  rgbaArrayToRGBAColor,
  toGammaSpace,
  toLinearSpace,
} from '../Colors';

import type {
  AnimatedStyle,
  SharedValue,
  AnimatableValue,
  Animation,
  AnimationObject,
  Timestamp,
  AnimatableValueObject,
} from '../commonTypes';
import NativeReanimatedModule from '../NativeReanimated';
import {
  AffineMatrixFlat,
  AffineMatrix,
  flatten,
  multiplyMatrices,
  scaleMatrix,
  addMatrices,
  decomposeMatrixIntoMatricesAndAngles,
  isAffineMatrixFlat,
  subtractMatrices,
  getRotationMatrix,
} from './transformationMatrix/matrixUtils';

let IN_STYLE_UPDATER = false;

export type UserUpdater = () => AnimatedStyle;

export function initialUpdaterRun<T>(updater: () => T): T {
  IN_STYLE_UPDATER = true;
  const result = updater();
  IN_STYLE_UPDATER = false;
  return result;
}

interface RecognizedPrefixSuffix {
  prefix?: string;
  suffix?: string;
  strippedValue: number;
}

function recognizePrefixSuffix(value: string | number): RecognizedPrefixSuffix {
  'worklet';
  if (typeof value === 'string') {
    const match = value.match(
      /([A-Za-z]*)(-?\d*\.?\d*)([eE][-+]?[0-9]+)?([A-Za-z%]*)/
    );
    if (!match) {
      throw Error(
        "Couldn't parse animation value. Check if there isn't any typo."
      );
    }
    const prefix = match[1];
    const suffix = match[4];
    // number with scientific notation
    const number = match[2] + (match[3] ?? '');
    return { prefix, suffix, strippedValue: parseFloat(number) };
  } else {
    return { strippedValue: value };
  }
}

function applyProgressToMatrix(
  progress: number,
  a: AffineMatrix,
  b: AffineMatrix
) {
  'worklet';
  return addMatrices(a, scaleMatrix(subtractMatrices(b, a), progress));
}

function applyProgressToNumber(progress: number, a: number, b: number) {
  'worklet';
  return a + progress * (b - a);
}

function decorateAnimation<T extends AnimationObject | StyleLayoutAnimation>(
  animation: T
): void {
  'worklet';
  if ((animation as HigherOrderAnimation).isHigherOrder) {
    return;
  }

  const baseOnStart = (animation as Animation<AnimationObject>).onStart;
  const baseOnFrame = (animation as Animation<AnimationObject>).onFrame;
  const animationCopy = Object.assign({}, animation);
  delete animationCopy.callback;

  const prefNumberSuffOnStart = (
    animation: Animation<AnimationObject>,
    value: string | number,
    timestamp: number,
    previousAnimation: Animation<AnimationObject>
  ) => {
    // recognize prefix, suffix, and updates stripped value on animation start
    const { prefix, suffix, strippedValue } = recognizePrefixSuffix(value);
    animation.__prefix = prefix;
    animation.__suffix = suffix;
    animation.strippedCurrent = strippedValue;
    const { strippedValue: strippedToValue } = recognizePrefixSuffix(
      animation.toValue as string | number
    );
    animation.current = strippedValue;
    animation.startValue = strippedValue;
    animation.toValue = strippedToValue;
    if (previousAnimation && previousAnimation !== animation) {
      const {
        prefix: paPrefix,
        suffix: paSuffix,
        strippedValue: paStrippedValue,
      } = recognizePrefixSuffix(previousAnimation.current as string | number);
      previousAnimation.current = paStrippedValue;
      previousAnimation.__prefix = paPrefix;
      previousAnimation.__suffix = paSuffix;
    }

    baseOnStart(animation, strippedValue, timestamp, previousAnimation);

    animation.current =
      (animation.__prefix ?? '') +
      animation.current +
      (animation.__suffix ?? '');

    if (previousAnimation && previousAnimation !== animation) {
      previousAnimation.current =
        (previousAnimation.__prefix ?? '') +
        previousAnimation.current +
        (previousAnimation.__suffix ?? '');
    }
  };
  const prefNumberSuffOnFrame = (
    animation: Animation<AnimationObject>,
    timestamp: number
  ) => {
    animation.current = animation.strippedCurrent;
    const res = baseOnFrame(animation, timestamp);
    animation.strippedCurrent = animation.current;
    animation.current =
      (animation.__prefix ?? '') +
      animation.current +
      (animation.__suffix ?? '');
    return res;
  };

  const tab = ['R', 'G', 'B', 'A'];
  const colorOnStart = (
    animation: Animation<AnimationObject>,
    value: string | number,
    timestamp: Timestamp,
    previousAnimation: Animation<AnimationObject>
  ): void => {
    let RGBAValue: ParsedColorArray;
    let RGBACurrent: ParsedColorArray;
    let RGBAToValue: ParsedColorArray;
    const res: Array<number> = [];
    if (isColor(value)) {
      RGBACurrent = toLinearSpace(convertToRGBA(animation.current));
      RGBAValue = toLinearSpace(convertToRGBA(value));
      if (animation.toValue) {
        RGBAToValue = toLinearSpace(convertToRGBA(animation.toValue));
      }
    }
    tab.forEach((i, index) => {
      animation[i] = Object.assign({}, animationCopy);
      animation[i].current = RGBACurrent[index];
      animation[i].toValue = RGBAToValue ? RGBAToValue[index] : undefined;
      animation[i].onStart(
        animation[i],
        RGBAValue[index],
        timestamp,
        previousAnimation ? previousAnimation[i] : undefined
      );
      res.push(animation[i].current);
    });

    animation.current = rgbaArrayToRGBAColor(
      toGammaSpace(res as ParsedColorArray)
    );
  };

  const colorOnFrame = (
    animation: Animation<AnimationObject>,
    timestamp: Timestamp
  ): boolean => {
    const RGBACurrent = toLinearSpace(convertToRGBA(animation.current));
    const res: Array<number> = [];
    let finished = true;
    tab.forEach((i, index) => {
      animation[i].current = RGBACurrent[index];
      const result = animation[i].onFrame(animation[i], timestamp);
      // We really need to assign this value to result, instead of passing it directly - otherwise once "finished" is false, onFrame won't be called
      finished &&= result;
      res.push(animation[i].current);
    });

    animation.current = rgbaArrayToRGBAColor(
      toGammaSpace(res as ParsedColorArray)
    );
    return finished;
  };

  const transformationMatrixOnStart = (
    animation: Animation<AnimationObject>,
    value: AffineMatrixFlat,
    timestamp: Timestamp,
    previousAnimation: Animation<AnimationObject>
  ): void => {
    const toValue = animation.toValue as AffineMatrixFlat;

    animation.startMatrices = decomposeMatrixIntoMatricesAndAngles(value);
    animation.stopMatrices = decomposeMatrixIntoMatricesAndAngles(toValue);

    // We create an animation copy to animate single value between 0 and 100
    // We set limits from 0 to 100 (instead of 0-1) to make spring look good
    // with default thresholds.

    animation[0] = Object.assign({}, animationCopy);
    animation[0].current = 0;
    animation[0].toValue = 100;
    animation[0].onStart(
      animation[0],
      0,
      timestamp,
      previousAnimation ? previousAnimation[0] : undefined
    );

    animation.current = value;
  };

  const transformationMatrixOnFrame = (
    animation: Animation<AnimationObject>,
    timestamp: Timestamp
  ): boolean => {
    let finished = true;
    const result = animation[0].onFrame(animation[0], timestamp);
    // We really need to assign this value to result, instead of passing it directly - otherwise once "finished" is false, onFrame won't be called
    finished &&= result;

    const progress = animation[0].current / 100;

    const transforms = ['translationMatrix', 'scaleMatrix', 'skewMatrix'];
    const mappedTransforms: Array<AffineMatrix> = [];

    transforms.forEach((key, _) =>
      mappedTransforms.push(
        applyProgressToMatrix(
          progress,
          animation.startMatrices[key],
          animation.stopMatrices[key]
        )
      )
    );

    const [currentTranslation, currentScale, skewMatrix] = mappedTransforms;

    const rotations: Array<'x' | 'y' | 'z'> = ['x', 'y', 'z'];
    const mappedRotations: Array<AffineMatrix> = [];

    rotations.forEach((key, _) => {
      const angle = applyProgressToNumber(
        progress,
        animation.startMatrices['r' + key],
        animation.stopMatrices['r' + key]
      );
      mappedRotations.push(getRotationMatrix(angle, key));
    });

    const [rotationMatrixX, rotationMatrixY, rotationMatrixZ] = mappedRotations;

    const rotationMatrix = multiplyMatrices(
      rotationMatrixX,
      multiplyMatrices(rotationMatrixY, rotationMatrixZ)
    );

    const updated = flatten(
      multiplyMatrices(
        multiplyMatrices(
          currentScale,
          multiplyMatrices(skewMatrix, rotationMatrix)
        ),
        currentTranslation
      )
    );

    animation.current = updated;

    return finished;
  };

  const arrayOnStart = (
    animation: Animation<AnimationObject>,
    value: Array<number>,
    timestamp: Timestamp,
    previousAnimation: Animation<AnimationObject>
  ): void => {
    value.forEach((v, i) => {
      animation[i] = Object.assign({}, animationCopy);
      animation[i].current = v;
      animation[i].toValue = (animation.toValue as Array<number>)[i];
      animation[i].onStart(
        animation[i],
        v,
        timestamp,
        previousAnimation ? previousAnimation[i] : undefined
      );
    });

    animation.current = value;
  };

  const arrayOnFrame = (
    animation: Animation<AnimationObject>,
    timestamp: Timestamp
  ): boolean => {
    let finished = true;
    (animation.current as Array<number>).forEach((_, i) => {
      const result = animation[i].onFrame(animation[i], timestamp);
      // We really need to assign this value to result, instead of passing it directly - otherwise once "finished" is false, onFrame won't be called
      finished &&= result;
      (animation.current as Array<number>)[i] = animation[i].current;
    });

    return finished;
  };

  const objectOnStart = (
    animation: Animation<AnimationObject>,
    value: AnimatableValueObject,
    timestamp: Timestamp,
    previousAnimation: Animation<AnimationObject>
  ): void => {
    for (const key in value) {
      animation[key] = Object.assign({}, animationCopy);
      animation[key].onStart = animation.onStart;

      animation[key].current = value[key];
      animation[key].toValue = (animation.toValue as AnimatableValueObject)[
        key
      ];
      animation[key].onStart(
        animation[key],
        value[key],
        timestamp,
        previousAnimation ? previousAnimation[key] : undefined
      );
    }
    animation.current = value;
  };

  const objectOnFrame = (
    animation: Animation<AnimationObject>,
    timestamp: Timestamp
  ): boolean => {
    let finished = true;
    const newObject: AnimatableValueObject = {};
    for (const key in animation.current as AnimatableValueObject) {
      const result = animation[key].onFrame(animation[key], timestamp);
      // We really need to assign this value to result, instead of passing it directly - otherwise once "finished" is false, onFrame won't be called
      finished &&= result;
      newObject[key] = animation[key].current;
    }
    animation.current = newObject;
    return finished;
  };

  animation.onStart = (
    animation: Animation<AnimationObject>,
    value: number,
    timestamp: Timestamp,
    previousAnimation: Animation<AnimationObject>
  ) => {
    if (isColor(value)) {
      colorOnStart(animation, value, timestamp, previousAnimation);
      animation.onFrame = colorOnFrame;
      return;
    } else if (isAffineMatrixFlat(value)) {
      transformationMatrixOnStart(
        animation,
        value,
        timestamp,
        previousAnimation
      );
      animation.onFrame = transformationMatrixOnFrame;
      return;
    } else if (Array.isArray(value)) {
      arrayOnStart(animation, value, timestamp, previousAnimation);
      animation.onFrame = arrayOnFrame;
      return;
    } else if (typeof value === 'string') {
      prefNumberSuffOnStart(animation, value, timestamp, previousAnimation);
      animation.onFrame = prefNumberSuffOnFrame;
      return;
    } else if (typeof value === 'object' && value !== null) {
      objectOnStart(animation, value, timestamp, previousAnimation);
      animation.onFrame = objectOnFrame;
      return;
    }
    baseOnStart(animation, value, timestamp, previousAnimation);
  };
}

type AnimationToDecoration<
  T extends AnimationObject | StyleLayoutAnimation,
  U extends AnimationObject | StyleLayoutAnimation
> = T extends StyleLayoutAnimation
  ? Record<string, unknown>
  : U | (() => U) | AnimatableValue;

const IS_NATIVE = NativeReanimatedModule.native;

export function defineAnimation<
  T extends AnimationObject | StyleLayoutAnimation, // type that's supposed to be returned
  U extends AnimationObject | StyleLayoutAnimation = T // type that's received
>(starting: AnimationToDecoration<T, U>, factory: () => T): T {
  'worklet';
  if (IN_STYLE_UPDATER) {
    return starting as unknown as T;
  }
  const create = () => {
    'worklet';
    const animation = factory();
    decorateAnimation<U>(animation as unknown as U);
    return animation;
  };

  if (_WORKLET || !IS_NATIVE) {
    return create();
  }
  // @ts-ignore: eslint-disable-line
  return create;
}

export function cancelAnimation<T>(sharedValue: SharedValue<T>): void {
  'worklet';
  // setting the current value cancels the animation if one is currently running
  sharedValue.value = sharedValue.value; // eslint-disable-line no-self-assign
}

// TODO it should work only if there was no animation before.
export function withStartValue(
  startValue: AnimatableValue,
  animation: NextAnimation<AnimationObject>
): Animation<AnimationObject> {
  'worklet';
  return defineAnimation(startValue, () => {
    'worklet';
    if (!_WORKLET && typeof animation === 'function') {
      animation = animation();
    }
    (animation as Animation<AnimationObject>).current = startValue;
    return animation as Animation<AnimationObject>;
  });
}
