import {
  AncestorValueList,
  Props,
  ValueMap,
  PoseMap,
  PassiveValueMap,
  ReadValue,
  ReadValueFromSource,
  CreateValue,
  ConvertValue,
  ActivePoses,
  SetValueNative
} from '../types';
import { getPoseValues } from '../inc/selectors';
import { resolveProp } from './setter';
import { Pose } from '..';

type ValueFactoryProps<V, A, TD> = {
  poses: PoseMap<A, TD>;
  passive?: PassiveValueMap;
  ancestorValues: AncestorValueList<V>;
  readValue: ReadValue<V>;
  createValue: CreateValue<V>;
  convertValue: ConvertValue<V>;
  userSetValues: { [key: string]: V };
  initialPose?: string | string[];
  readValueFromSource?: ReadValueFromSource;
  setValueNative: SetValueNative;
  activePoses: ActivePoses;
  props: Props;
};

export const DEFAULT_INITIAL_POSE = 'init';

const isScale = (key: string) => key.includes('scale');
const defaultReadValueFromSource = (key: string) => (isScale(key) ? 1 : 0);

/**
 * This is a prime candidate for refactoring, possibly in combination
 * with the `posesToSearch.find()` function.
 */
const readValueFromPose = <A, TD>(
  pose: Pose<A, TD>,
  key: string,
  props: Props
) => {
  const valueToResolve =
    pose.applyAtEnd && pose.applyAtEnd[key] !== undefined
      ? pose.applyAtEnd[key]
      : pose[key] !== undefined
        ? pose[key]
        : pose.applyAtStart && pose.applyAtStart[key] !== undefined
          ? pose.applyAtStart[key]
          : 0;

  return resolveProp(valueToResolve, props);
};

const getPosesToSearch = (pose: string | string[]) => {
  const posesToSearch = Array.isArray(pose) ? pose : [pose];
  posesToSearch.push(DEFAULT_INITIAL_POSE);
  return posesToSearch;
};

const getInitialValue = <A, TD>(
  poses: PoseMap<A, TD>,
  key: string,
  initialPose: string | string[],
  props: Props,
  readValueFromSource: ReadValueFromSource = defaultReadValueFromSource,
  activePoses: ActivePoses
) => {
  const posesToSearch = getPosesToSearch(initialPose);

  const pose: string = posesToSearch.filter(Boolean).find((name: string) => {
    const thisPose: Pose<A, TD> = poses[name];

    return (
      thisPose &&
      (thisPose[key] !== undefined ||
        (thisPose.applyAtStart && thisPose.applyAtStart[key] !== undefined) ||
        (thisPose.applyAtEnd && thisPose.applyAtEnd[key] !== undefined))
    );
  });

  // Prime active values array with found pose as first item
  // TODO: Instead of priming with 'init' if no pose found, create
  // a unique pose with the readValueFromSource return value
  activePoses.set(key, [pose || DEFAULT_INITIAL_POSE]);

  return pose
    ? readValueFromPose(poses[pose], key, props)
    : readValueFromSource(key, props);
};

const createValues = <V, A, TD>(
  values: ValueMap<V>,
  {
    userSetValues,
    createValue,
    convertValue,
    readValueFromSource,
    initialPose,
    poses,
    activePoses,
    props
  }: ValueFactoryProps<V, A, TD>
) => (key: string) => {
  if (values.has(key)) return;

  let value: V;

  // If this user has explicitly created a value, use that
  if (userSetValues && userSetValues[key] !== undefined) {
    value = convertValue(userSetValues[key], key, props);

    // Or create a new value
  } else {
    const initValue = getInitialValue(
      poses,
      key,
      initialPose,
      props,
      readValueFromSource,
      activePoses
    );

    value = createValue(initValue, key, props);
  }

  values.set(key, value);
};

const scrapeValuesFromPose = <V, A, TD>(
  values: ValueMap<V>,
  props: ValueFactoryProps<V, A, TD>
) => (key: string) => {
  const pose = props.poses[key];
  Object.keys(getPoseValues<A, TD>(pose)).forEach(
    createValues<V, A, TD>(values, props)
  );
};

const getAncestorValue = <V>(
  key: string,
  fromParent: boolean | string,
  ancestors: AncestorValueList<V>
): V => {
  if (fromParent === true) {
    return ancestors[0] && ancestors[0].values.get(key);
  } else {
    const foundAncestor = ancestors.find(({ label }) => label === fromParent);
    return foundAncestor && foundAncestor.values.get(key);
  }
};

const bindPassiveValues = <V, A, TD>(
  values: ValueMap<V>,
  {
    passive,
    ancestorValues,
    createValue,
    readValue,
    props
  }: ValueFactoryProps<V, A, TD>
) => (key: string) => {
  const [valueKey, passiveProps, fromParent] = passive[key];
  const valueToBind =
    fromParent && ancestorValues.length
      ? getAncestorValue<V>(valueKey, fromParent, ancestorValues)
      : values.has(valueKey)
        ? values.get(valueKey)
        : false;

  if (!valueToBind) return;

  const newValue = createValue(readValue(valueToBind), key, props, {
    passiveParentKey: valueKey,
    passiveParent: valueToBind,
    passiveProps
  });

  values.set(key, newValue);
};

const setNativeValues = <V, A, TD>({
  setValueNative,
  initialPose,
  props,
  poses
}: ValueFactoryProps<V, A, TD>) => {
  const valuesHaveSet = new Set();

  const setValues = (pose: Pose<A, TD>, propKey: string) => {
    if (pose[propKey]) {
      for (const key in pose[propKey]) {
        if (!valuesHaveSet.has(key)) {
          valuesHaveSet.add(key);
          setValueNative(key, resolveProp(pose[propKey][key], props), props);
        }
      }
    }
  };

  getPosesToSearch(initialPose).forEach(poseKey => {
    const pose = poses[poseKey];
    if (pose) {
      setValues(pose, 'applyAtEnd');
      setValues(pose, 'applyAtStart');
    }
  });
};

const createValueMap = <V, A, TD>(
  props: ValueFactoryProps<V, A, TD>
): ValueMap<V> => {
  const { poses, passive } = props;
  const values: ValueMap<V> = new Map();

  // Scrape values from poses
  Object.keys(poses).forEach(scrapeValuesFromPose<V, A, TD>(values, props));

  // Set any remaining values that won't be animated but need to be set initially
  // An optimisation here might be to have a mutative value in props or some other flag
  // so we know if we've found `applyAtStart` or `applyAtEnd` during value scraping
  setNativeValues(props);

  // Initiate passive values
  if (passive)
    Object.keys(passive).forEach(bindPassiveValues<V, A, TD>(values, props));

  return values;
};

export default createValueMap;
