import type {SignalRef} from 'vega';
import {getMainRangeChannel, getSecondaryRangeChannel, getSizeChannel, getVgPositionChannel} from '../../../channel.js';
import {isFieldOrDatumDef} from '../../../channeldef.js';
import * as log from '../../../log/index.js';
import {isRelativeBandSize, Mark, MarkConfig, MarkDef} from '../../../mark.js';
import {VgEncodeEntry, VgValueRef} from '../../../vega.schema.js';
import {getMarkStyleConfig} from '../../common.js';
import {UnitModel} from '../../unit.js';
import {positionOffset} from './offset.js';
import {vgAlignedPositionChannel} from './position-align.js';
import {pointPosition, pointPositionDefaultRef} from './position-point.js';
import * as ref from './valueref.js';

/**
 * Utility for area/rule position, which can be either point or range.
 * (One of the axes should be point and the other should be range.)
 */
export function pointOrRangePosition(
  channel: 'x' | 'y',
  model: UnitModel,
  {
    defaultPos,
    defaultPos2,
    range,
  }: {
    defaultPos: 'zeroOrMin' | 'zeroOrMax' | 'mid';
    defaultPos2: 'zeroOrMin' | 'zeroOrMax';
    range: boolean;
  },
) {
  if (range) {
    return rangePosition(channel, model, {defaultPos, defaultPos2});
  }
  return pointPosition(channel, model, {defaultPos});
}

export function rangePosition(
  channel: 'x' | 'y' | 'theta' | 'radius',
  model: UnitModel,
  {
    defaultPos,
    defaultPos2,
  }: {
    defaultPos: 'zeroOrMin' | 'zeroOrMax' | 'mid';
    defaultPos2: 'zeroOrMin' | 'zeroOrMax';
  },
): VgEncodeEntry {
  const {markDef, config} = model;
  const channel2 = getSecondaryRangeChannel(channel);
  const sizeChannel = getSizeChannel(channel);

  const pos2Mixins = pointPosition2OrSize(model, defaultPos2, channel2);

  const vgChannel = pos2Mixins[sizeChannel]
    ? // If there is width/height, we need to position the marks based on the alignment.
      vgAlignedPositionChannel(channel, markDef, config)
    : // Otherwise, make sure to apply to the right Vg Channel (for arc mark)
      getVgPositionChannel(channel);

  return {
    ...pointPosition(channel, model, {defaultPos, vgChannel}),
    ...pos2Mixins,
  };
}

/**
 * Return encode for x2, y2.
 * If channel is not specified, return one channel based on orientation.
 */
function pointPosition2OrSize(
  model: UnitModel,
  defaultPos: 'zeroOrMin' | 'zeroOrMax',
  channel: 'x2' | 'y2' | 'radius2' | 'theta2',
) {
  const {encoding, mark, markDef, stack, config} = model;

  const baseChannel = getMainRangeChannel(channel);
  const sizeChannel = getSizeChannel(channel);
  const vgChannel = getVgPositionChannel(channel);

  const channelDef = encoding[baseChannel];
  const scaleName = model.scaleName(baseChannel);
  const scale = model.getScaleComponent(baseChannel);

  const {offset} =
    channel in encoding || channel in markDef
      ? positionOffset({channel, markDef, encoding, model})
      : positionOffset({channel: baseChannel, markDef, encoding, model});

  if (!channelDef && (channel === 'x2' || channel === 'y2') && (encoding.latitude || encoding.longitude)) {
    const vgSizeChannel = getSizeChannel(channel);

    const size = model.markDef[vgSizeChannel];
    if (size != null) {
      return {
        [vgSizeChannel]: {value: size},
      };
    } else {
      return {
        [vgChannel]: {field: model.getName(channel)},
      };
    }
  }

  const valueRef = position2Ref({
    channel,
    channelDef,
    channel2Def: encoding[channel],
    markDef,
    config,
    scaleName,
    scale,
    stack,
    offset,
    defaultRef: undefined,
  });

  if (valueRef !== undefined) {
    return {[vgChannel]: valueRef};
  }

  // TODO: check width/height encoding here once we add them

  // no x2/y2 encoding, then try to read x2/y2 or width/height based on precedence:
  // markDef > config.style > mark-specific config (config[mark]) > general mark config (config.mark)

  return (
    position2orSize(channel, markDef) ||
    position2orSize(channel, {
      [channel]: getMarkStyleConfig(channel, markDef, config.style),
      [sizeChannel]: getMarkStyleConfig(sizeChannel, markDef, config.style),
    }) ||
    position2orSize(channel, config[mark]) ||
    position2orSize(channel, config.mark) || {
      [vgChannel]: pointPositionDefaultRef({
        model,
        defaultPos,
        channel,
        scaleName,
        scale,
      })(),
    }
  );
}

export function position2Ref({
  channel,
  channelDef,
  channel2Def,
  markDef,
  config,
  scaleName,
  scale,
  stack,
  offset,
  defaultRef,
}: ref.MidPointParams & {
  channel: 'x2' | 'y2' | 'radius2' | 'theta2';
}): VgValueRef | VgValueRef[] {
  if (
    isFieldOrDatumDef(channelDef) &&
    stack &&
    // If fieldChannel is X and channel is X2 (or Y and Y2)
    channel.charAt(0) === stack.fieldChannel.charAt(0)
  ) {
    return ref.valueRefForFieldOrDatumDef(channelDef, scaleName, {suffix: 'start'}, {offset});
  }
  return ref.midPointRefWithPositionInvalidTest({
    channel,
    channelDef: channel2Def,
    scaleName,
    scale,
    stack,
    markDef,
    config,
    offset,
    defaultRef,
  });
}

function position2orSize(
  channel: 'x2' | 'y2' | 'radius2' | 'theta2',
  markDef: MarkConfig<SignalRef> | MarkDef<Mark, SignalRef>,
) {
  const sizeChannel = getSizeChannel(channel);
  const vgChannel = getVgPositionChannel(channel);
  if (markDef[vgChannel] !== undefined) {
    return {[vgChannel]: ref.widthHeightValueOrSignalRef(channel, markDef[vgChannel])};
  } else if (markDef[channel] !== undefined) {
    return {[vgChannel]: ref.widthHeightValueOrSignalRef(channel, markDef[channel])};
  } else if (markDef[sizeChannel]) {
    const dimensionSize = markDef[sizeChannel];
    if (isRelativeBandSize(dimensionSize)) {
      log.warn(log.message.relativeBandSizeNotSupported(sizeChannel));
    } else {
      return {[sizeChannel]: ref.widthHeightValueOrSignalRef(channel, dimensionSize)};
    }
  }
  return undefined;
}
