import defaults from "lodash/defaults";
import { Helpers, Scale, Axis } from "victory-core";
import { VictoryAxisProps } from "./victory-axis";

const orientationSign = {
  top: -1,
  left: -1,
  right: 1,
  bottom: 1,
};

const exists = (val) => val !== null && val !== undefined;

const getCurrentAxis = (props, axis) => {
  const { orientation, horizontal } = props;
  if (orientation) {
    const dimensions = { top: "x", bottom: "x", left: "y", right: "y" };
    return dimensions[orientation];
  }
  const otherAxis = axis === "x" ? "y" : "x";
  return horizontal ? otherAxis : axis;
};

const getScale = (props) => {
  const axis = Axis.getAxis(props);
  const currentAxis = getCurrentAxis(props, axis);
  const scale = Scale.getBaseScale(props, axis);
  const propsDomain = props.domain && props.domain[axis];
  const domain = propsDomain || Axis.getDomain(props) || scale.domain();
  scale.range(Helpers.getRange(props, currentAxis));
  scale.domain(domain);
  return scale;
};

const getStyleObject = (props) => {
  const { theme, dependentAxis } = props;
  const generalAxisStyle = theme && theme.axis && theme.axis.style;
  const axisType = dependentAxis ? "dependentAxis" : "independentAxis";
  const specificAxisStyle = theme && theme[axisType] && theme[axisType].style;

  const mergeStyles = () => {
    const styleNamespaces = [
      "axis",
      "axisLabel",
      "grid",
      "parent",
      "tickLabels",
      "ticks",
    ];
    return styleNamespaces.reduce((memo, curr) => {
      memo[curr] = defaults(
        {},
        specificAxisStyle[curr],
        generalAxisStyle[curr],
      );
      return memo;
    }, {});
  };

  return generalAxisStyle && specificAxisStyle
    ? mergeStyles()
    : specificAxisStyle || generalAxisStyle;
};

export const getStyles = (
  props,
  styleObject: VictoryAxisProps["style"] = {},
) => {
  const style = props.style || {};
  const parentStyleProps = { height: "100%", width: "100%" };
  return {
    parent: defaults(style.parent, styleObject.parent, parentStyleProps),
    axis: defaults({}, style.axis, styleObject.axis),
    axisLabel: defaults({}, style.axisLabel, styleObject.axisLabel),
    grid: defaults({}, style.grid, styleObject.grid),
    ticks: defaults({}, style.ticks, styleObject.ticks),
    tickLabels: defaults({}, style.tickLabels, styleObject.tickLabels),
  };
};

const getTickProps = (layout, style, datum) => {
  const { position, transform } = layout;
  return {
    x1: transform.x,
    y1: transform.y,
    x2: transform.x + position.x2,
    y2: transform.y + position.y2,
    style,
    datum,
  };
};

// eslint-disable-next-line max-params
const getTickLabelProps = (layout, style, anchors, datum, text) => {
  const { position, transform } = layout;
  return {
    style,
    x: transform.x + position.x,
    y: transform.y + position.y,
    verticalAnchor: anchors.verticalAnchor,
    textAnchor: anchors.textAnchor,
    angle: style.angle,
    text,
    datum,
  };
};

const getGridProps = (layout, style, datum) => {
  const { edge, transform } = layout;
  return {
    x1: transform.x,
    y1: transform.y,
    x2: edge.x + transform.x,
    y2: edge.y + transform.y,
    style,
    datum,
  };
};

const getAxisProps = (modifiedProps, calculatedValues, globalTransform) => {
  const { style, padding, isVertical } = calculatedValues;
  const { width, height } = modifiedProps;
  return {
    style: style.axis,
    x1: isVertical ? globalTransform.x : padding.left + globalTransform.x,
    x2: isVertical
      ? globalTransform.x
      : width - padding.right + globalTransform.x,
    y1: isVertical ? padding.top + globalTransform.y : globalTransform.y,
    y2: isVertical
      ? height - padding.bottom + globalTransform.y
      : globalTransform.y,
  };
};

const getEvaluatedStyles = (style, props) => {
  return {
    tickStyle: Helpers.evaluateStyle(style.ticks, props),
    labelStyle: Helpers.evaluateStyle(style.tickLabels, props),
    gridStyle: Helpers.evaluateStyle(style.grid, props),
  };
};

const getAxisLabelProps = (props, calculatedValues, globalTransform) => {
  const { style, orientation, padding, labelPadding, isVertical } =
    calculatedValues;
  const sign = orientationSign[orientation];
  const hPadding = padding.left + padding.right;
  const vPadding = padding.top + padding.bottom;
  const verticalAnchor = sign < 0 ? "end" : "start";
  const labelStyle = style.axisLabel;
  const angle = isVertical ? -90 : 0; // eslint-disable-line no-magic-numbers
  const x = isVertical
    ? globalTransform.x + sign * labelPadding
    : (props.width - hPadding) / 2 + padding.left + globalTransform.x;
  const y = isVertical
    ? (props.height - vPadding) / 2 + padding.top + globalTransform.y
    : sign * labelPadding + globalTransform.y;

  return {
    x,
    y,
    verticalAnchor: labelStyle.verticalAnchor || verticalAnchor,
    textAnchor: labelStyle.textAnchor || "middle",
    angle: labelStyle.angle === undefined ? angle : labelStyle.angle,
    style: labelStyle,
    text: props.label,
  };
};

const getAnchors = (orientation, isVertical) => {
  const anchorOrientation = {
    top: "end",
    left: "end",
    right: "start",
    bottom: "start",
  };
  const anchor = anchorOrientation[orientation];
  return {
    textAnchor: isVertical ? anchor : "middle",
    verticalAnchor: isVertical ? "middle" : anchor,
  };
};

const getLabelPadding = (props, style) => {
  const labelStyle = style.axisLabel || {};
  if (labelStyle.padding !== undefined && labelStyle.padding !== null) {
    return labelStyle.padding;
  }
  const isVertical = Axis.isVertical(props);
  // TODO: magic numbers
  /* eslint-disable no-magic-numbers*/
  const fontSize = labelStyle.fontSize || 14;
  return props.label ? fontSize * (isVertical ? 2.3 : 1.6) : 0;
  /* eslint-enable no-magic-numbers*/
};

const getDefaultOrientations = (axis, originSign, horizontal) => {
  const sign = originSign || "positive";
  const orientations = {
    positive: { x: "bottom", y: "left" },
    negative: { x: "top", y: "right" },
  };
  const horizontalOrientations = {
    positive: { x: "left", y: "bottom" },
    negative: { x: "right", y: "top" },
  };
  return horizontal
    ? horizontalOrientations[sign][axis]
    : orientations[sign][axis];
};

const getStandaloneOffset = (props, calculatedValues) => {
  const {
    style,
    scale,
    orientation,
    padding,
    axis,
    ticks,
    stringTicks,
    isVertical,
    labelPadding,
  } = calculatedValues;
  const { polar, horizontal } = props;
  const sharedProps = {
    scale: { [axis]: scale },
    polar,
    horizontal,
    ticks,
    stringTicks,
  };
  const xPadding = orientation === "right" ? padding.right : padding.left;
  const yPadding = orientation === "top" ? padding.top : padding.bottom;
  const offsetX =
    props.offsetX !== null && props.offsetX !== undefined
      ? props.offsetX
      : xPadding;
  const offsetY =
    props.offsetY !== null && props.offsetY !== undefined
      ? props.offsetY
      : yPadding;
  const fontSize = style.axisLabel.fontSize || 14; // eslint-disable-line no-magic-numbers
  const tickSizes = ticks.map((data, index) => {
    const tick = stringTicks ? props.tickValues[data - 1] : data;
    const tickStyle = Helpers.evaluateStyle(
      style.ticks,
      Object.assign({}, sharedProps, { tick, index }),
    );
    return tickStyle.size || 0;
  });
  const totalPadding = fontSize + 2 * Math.max(...tickSizes) + labelPadding;
  const minimumPadding = 1.2 * fontSize; // eslint-disable-line no-magic-numbers
  const x = isVertical ? totalPadding : minimumPadding;
  const y = isVertical ? minimumPadding : totalPadding;

  return {
    x: offsetX !== null && offsetX !== undefined ? offsetX : x,
    y: offsetY !== null && offsetY !== undefined ? offsetY : y,
  };
};

const isEqual = (a, b) => {
  if (a instanceof Date && b instanceof Date) {
    return a.getTime() === b.getTime();
  }
  return a === b;
};

const getOffset = (props, calculatedValues) => {
  const { scale, origin, orientation, orientations, domain, padding } =
    calculatedValues;
  const { top, bottom, left, right } = padding;

  const calculatedOrientation = {
    x:
      orientation === "bottom" || orientation === "top"
        ? orientation
        : orientations.x,
    y:
      orientation === "left" || orientation === "right"
        ? orientation
        : orientations.y,
  };

  // make the axes line up, and cross when appropriate
  const orientationOffset = {
    x: calculatedOrientation.y === "left" ? left : right,
    y: calculatedOrientation.x === "bottom" ? bottom : top,
  };
  const originOffset = {
    x: calculatedOrientation.y === "left" ? 0 : props.width,
    y: calculatedOrientation.x === "bottom" ? props.height : 0,
  };
  const originPosition = {
    x:
      isEqual(origin.x, domain.x[0]) || isEqual(origin.x, domain.x[1])
        ? 0
        : scale.x(origin.x),
    y:
      isEqual(origin.y, domain.y[0]) || isEqual(origin.y, domain.y[1])
        ? 0
        : scale.y(origin.y),
  };
  const x = originPosition.x
    ? Math.abs(originOffset.x - originPosition.x)
    : orientationOffset.x;
  const y = originPosition.y
    ? Math.abs(originOffset.y - originPosition.y)
    : orientationOffset.y;
  const offsetX = exists(props.offsetX) ? props.offsetX : x;
  const offsetY = exists(props.offsetY) ? props.offsetY : y;

  return {
    x: offsetX,
    y: offsetY,
  };
};

const getHorizontalOffset = (props, calculatedValues) => {
  const { scale, origin, orientation, orientations, domain, padding } =
    calculatedValues;
  const { top, bottom, left, right } = padding;

  const calculatedOrientation = {
    y:
      orientation === "bottom" || orientation === "top"
        ? orientation
        : orientations.x,
    x:
      orientation === "left" || orientation === "right"
        ? orientation
        : orientations.y,
  };

  // make the axes line up, and cross when appropriate
  const orientationOffset = {
    x: calculatedOrientation.y === "bottom" ? bottom : top,
    y: calculatedOrientation.x === "left" ? left : right,
  };
  const originOffset = {
    y: calculatedOrientation.x === "left" ? 0 : props.width,
    x: calculatedOrientation.y === "bottom" ? props.height : 0,
  };
  const originPosition = {
    x:
      isEqual(origin.x, domain.x[0]) || isEqual(origin.x, domain.x[1])
        ? 0
        : scale.x(origin.x),
    y:
      isEqual(origin.y, domain.y[0]) || isEqual(origin.y, domain.y[1])
        ? 0
        : scale.y(origin.y),
  };

  const y = originPosition.x
    ? Math.abs(originOffset.x - originPosition.x)
    : orientationOffset.x;
  const x = originPosition.y
    ? Math.abs(originOffset.y - originPosition.y)
    : orientationOffset.y;
  const offsetX = exists(props.offsetX) ? props.offsetX : x;
  const offsetY = exists(props.offsetY) ? props.offsetY : y;

  return {
    x: offsetX,
    y: offsetY,
  };
};

const getTransform = (props, calculatedValues, offset) => {
  const { orientation, axis } = calculatedValues;
  const axisValue = Axis.getAxisValue(props, axis);
  return {
    top: {
      x: 0,
      y: axisValue !== undefined ? axisValue : offset.y,
    },
    bottom: {
      x: 0,
      y: axisValue !== undefined ? axisValue : props.height - offset.y,
    },
    left: {
      x: axisValue !== undefined ? axisValue : offset.x,
      y: 0,
    },
    right: {
      x: axisValue !== undefined ? axisValue : props.width - offset.x,
      y: 0,
    },
  }[orientation];
};

const getTickPosition = (style, orientation, isVertical) => {
  const { tickStyle, labelStyle } = style;
  const size = tickStyle.size || 0;
  const tickPadding = tickStyle.padding || 0;
  const labelPadding = labelStyle.padding || 0;
  const tickSpacing = size + tickPadding + labelPadding;
  const sign = orientationSign[orientation];
  return {
    x: isVertical ? sign * tickSpacing : 0,
    x2: isVertical ? sign * size : 0,
    y: isVertical ? 0 : sign * tickSpacing,
    y2: isVertical ? 0 : sign * size,
  };
};

const getTickTransform = (tick, globalTransform, isVertical) => {
  return {
    x: isVertical ? globalTransform.x : tick + globalTransform.x,
    y: isVertical ? tick + globalTransform.y : globalTransform.y,
  };
};

const getGridEdge = (props, calculatedValues) => {
  const { orientation, padding, isVertical } = calculatedValues;
  const sign = -orientationSign[orientation];
  const x = isVertical
    ? sign * (props.width - (padding.left + padding.right))
    : 0;
  const y = isVertical
    ? 0
    : sign * (props.height - (padding.top + padding.bottom));
  return { x, y };
};

const getGridOffset = (calculatedValues, offset) => {
  const { padding, orientation, crossAxis } = calculatedValues;
  const xPadding = orientation === "right" ? padding.right : padding.left;
  const yPadding = orientation === "top" ? padding.top : padding.bottom;
  return {
    x: crossAxis ? offset.x - xPadding : 0,
    y: crossAxis ? offset.y - yPadding : 0,
  };
};

const getLayoutProps = (modifiedProps, calculatedValues) => {
  let offset;
  if (calculatedValues.domain.x && calculatedValues.domain.y) {
    offset = modifiedProps.horizontal
      ? getHorizontalOffset(modifiedProps, calculatedValues)
      : getOffset(modifiedProps, calculatedValues);
  } else {
    offset = getStandaloneOffset(modifiedProps, calculatedValues);
  }
  return {
    globalTransform: getTransform(modifiedProps, calculatedValues, offset),
    gridOffset: getGridOffset(calculatedValues, offset),
    gridEdge: getGridEdge(modifiedProps, calculatedValues),
  };
};

const getOrientation = (props) => {
  if (props.orientation) {
    return props.orientation;
  }
  const defaultOrientations = {
    dependent: props.horizontal ? "bottom" : "left",
    independent: props.horizontal ? "left" : "bottom",
  };
  return props.dependentAxis
    ? defaultOrientations.dependent
    : defaultOrientations.independent;
};

const getCalculatedValues = (props) => {
  const defaultStyles = getStyleObject(props);
  const style = getStyles(props, defaultStyles);
  const padding = Helpers.getPadding(props.padding);
  const labelPadding = getLabelPadding(props, style);
  const stringTicks = Axis.stringTicks(props) ? props.tickValues : undefined;
  const axis = Axis.getAxis(props);
  const axisDomain = Axis.getDomain(props);
  const axisScale = getScale(props);
  const xAxisDomain = axis === "x" ? axisDomain : undefined;
  const yAxisDomain = axis === "y" ? axisDomain : undefined;
  const xAxisScale = axis === "x" ? axisScale : undefined;
  const yAxisScale = axis === "y" ? axisScale : undefined;
  const crossAxis = !(props.crossAxis === false || props.standalone === true);
  const ticks = Axis.getTicks(props, axisScale, crossAxis);
  const tickFormat = Axis.getTickFormat(props, axisScale);
  const range = {
    x: Helpers.getRange(props, "x"),
    y: Helpers.getRange(props, "y"),
  };
  // use full domain if passed in from parent,
  // otherwise use the just the one axis available
  const domain = {
    x: props.domain && props.domain.x ? props.domain.x : xAxisDomain,
    y: props.domain && props.domain.y ? props.domain.y : yAxisDomain,
  };
  // use full scale if passed in from parent,
  // otherwise use the just the one axis available
  const scale = {
    x:
      props.domain && props.domain.x
        ? Scale.getBaseScale(props, "x")
            .domain(props.domain.x)
            .range(props.horizontal ? range.y : range.x)
        : xAxisScale,
    y:
      props.domain && props.domain.y
        ? Scale.getBaseScale(props, "y")
            .domain(props.domain.y)
            .range(props.horizontal ? range.x : range.y)
        : yAxisScale,
  };
  const origin = domain.x && domain.y ? Axis.getOrigin(domain) : undefined;
  const originSign = origin
    ? {
        x: Axis.getOriginSign(origin.x, domain.x),
        y: Axis.getOriginSign(origin.y, domain.y),
      }
    : undefined;
  const orientations = originSign
    ? {
        x: getDefaultOrientations("x", originSign.y, props.horizontal),
        y: getDefaultOrientations("y", originSign.x, props.horizontal),
      }
    : undefined;
  const orientation = orientations
    ? props.orientation || orientations[axis]
    : getOrientation(props);
  const isVertical = Axis.isVertical(Object.assign({}, props, { orientation }));
  const anchors = getAnchors(orientation, isVertical);
  return {
    anchors,
    axis,
    crossAxis,
    domain,
    isVertical,
    labelPadding,
    orientation,
    orientations,
    origin,
    padding,
    scale,
    stringTicks,
    style,
    tickFormat,
    ticks,
  };
};

export const getBaseProps = (initialProps, fallbackProps) => {
  const props = Axis.modifyProps(initialProps, fallbackProps);
  const calculatedValues = getCalculatedValues(props);
  const {
    axis,
    style,
    orientation,
    isVertical,
    scale,
    ticks,
    tickFormat,
    anchors,
    domain,
    stringTicks,
  } = calculatedValues;
  const otherAxis = axis === "x" ? "y" : "x";
  const { width, height, standalone, theme, polar, padding, horizontal } =
    props;
  const { globalTransform, gridOffset, gridEdge } = getLayoutProps(
    props,
    calculatedValues,
  );
  const sharedProps = {
    scale: { [axis]: scale[axis] },
    polar,
    horizontal,
    ticks,
    stringTicks,
  };
  const axisProps = getAxisProps(props, calculatedValues, globalTransform);
  const axisLabelProps = getAxisLabelProps(
    props,
    calculatedValues,
    globalTransform,
  );
  const initialChildProps = {
    parent: Object.assign(
      {
        style: style.parent,
        ticks,
        standalone,
        theme,
        width,
        height,
        padding,
        domain,
      },
      sharedProps,
    ),
  };
  const gridProps = {
    dimension: otherAxis,
    range: { [otherAxis]: Helpers.getRange(props, otherAxis) },
    scale:
      props.scale && props.scale[otherAxis]
        ? { [otherAxis]: props.scale[otherAxis] }
        : undefined,
  };

  return (ticks as number[]).reduce((childProps, tickValue, index) => {
    const tick = stringTicks ? stringTicks[index] : tickValue;
    const text = tickFormat(tickValue, index, ticks);
    const styles = getEvaluatedStyles(
      style,
      Object.assign({}, sharedProps, { tick, tickValue, index, text }),
    );
    const tickLayout = {
      position: getTickPosition(styles, orientation, isVertical),
      transform: getTickTransform(
        scale[axis]?.(tickValue),
        globalTransform,
        isVertical,
      ),
    };

    const gridLayout = {
      edge: gridEdge,
      transform: {
        x: isVertical
          ? -gridOffset.x + globalTransform.x
          : scale[axis]?.(tickValue) + globalTransform.x,
        y: isVertical
          ? scale[axis]?.(tickValue) + globalTransform.y
          : gridOffset.y + globalTransform.y,
      },
    };
    childProps[index] = {
      axis: Object.assign({ dimension: axis }, sharedProps, axisProps),
      axisLabel: Object.assign({}, sharedProps, axisLabelProps),
      ticks: Object.assign(
        {},
        sharedProps,
        getTickProps(tickLayout, styles.tickStyle, tickValue),
      ),
      tickLabels: Object.assign(
        {},
        sharedProps,
        getTickLabelProps(
          tickLayout,
          styles.labelStyle,
          anchors,
          tickValue,
          text,
        ),
      ),
      grid: Object.assign(
        {},
        sharedProps,
        gridProps,
        getGridProps(gridLayout, styles.gridStyle, tickValue),
      ),
    };
    return childProps;
  }, initialChildProps);
};
