/*
 * Constants and utilities for encoding channels (Visual variables)
 * such as 'x', 'y', 'color'.
 */

import {hasOwnProperty} from 'vega-util';
import {RangeType} from './compile/scale/type.js';
import {Encoding} from './encoding.js';
import {Mark} from './mark.js';
import {EncodingFacetMapping} from './spec/facet.js';
import {Flag, keys} from './util.js';

export type Channel = keyof Encoding<any>;
export type ExtendedChannel = Channel | FacetChannel;

// Facet
export const ROW = 'row' as const;
export const COLUMN = 'column' as const;

export const FACET = 'facet' as const;

// Position
export const X = 'x' as const;
export const Y = 'y' as const;
export const X2 = 'x2' as const;
export const Y2 = 'y2' as const;

// Position Offset
export const XOFFSET = 'xOffset' as const;
export const YOFFSET = 'yOffset' as const;

// Arc-Position
export const RADIUS = 'radius' as const;
export const RADIUS2 = 'radius2' as const;
export const THETA = 'theta' as const;
export const THETA2 = 'theta2' as const;

// Geo Position
export const LATITUDE = 'latitude' as const;
export const LONGITUDE = 'longitude' as const;
export const LATITUDE2 = 'latitude2' as const;
export const LONGITUDE2 = 'longitude2' as const;

// Time
export const TIME = 'time' as const;

// Mark property with scale
export const COLOR = 'color' as const;

export const FILL = 'fill' as const;

export const STROKE = 'stroke' as const;

export const SHAPE = 'shape' as const;
export const SIZE = 'size' as const;

export const ANGLE = 'angle' as const;

export const OPACITY = 'opacity' as const;
export const FILLOPACITY = 'fillOpacity' as const;

export const STROKEOPACITY = 'strokeOpacity' as const;

export const STROKEWIDTH = 'strokeWidth' as const;
export const STROKEDASH = 'strokeDash' as const;

// Non-scale channel
export const TEXT = 'text' as const;
export const ORDER = 'order' as const;
export const DETAIL = 'detail' as const;
export const KEY = 'key' as const;

export const TOOLTIP = 'tooltip' as const;
export const HREF = 'href' as const;

export const URL = 'url' as const;
export const DESCRIPTION = 'description' as const;

const POSITION_CHANNEL_INDEX = {
  x: 1,
  y: 1,
  x2: 1,
  y2: 1,
} as const;

export type PositionChannel = keyof typeof POSITION_CHANNEL_INDEX;

const POLAR_POSITION_CHANNEL_INDEX = {
  theta: 1,
  theta2: 1,
  radius: 1,
  radius2: 1,
} as const;

export type PolarPositionChannel = keyof typeof POLAR_POSITION_CHANNEL_INDEX;

export function isPolarPositionChannel(c: Channel): c is PolarPositionChannel {
  return hasOwnProperty(POLAR_POSITION_CHANNEL_INDEX, c);
}

const GEO_POSIITON_CHANNEL_INDEX = {
  longitude: 1,
  longitude2: 1,
  latitude: 1,
  latitude2: 1,
} as const;

export type GeoPositionChannel = keyof typeof GEO_POSIITON_CHANNEL_INDEX;

export function getPositionChannelFromLatLong(channel: GeoPositionChannel): PositionChannel {
  switch (channel) {
    case LATITUDE:
      return 'y';
    case LATITUDE2:
      return 'y2';
    case LONGITUDE:
      return 'x';
    case LONGITUDE2:
      return 'x2';
  }
}

export function isGeoPositionChannel(c: Channel): c is GeoPositionChannel {
  return hasOwnProperty(GEO_POSIITON_CHANNEL_INDEX, c);
}

export const GEOPOSITION_CHANNELS = keys(GEO_POSIITON_CHANNEL_INDEX);

const UNIT_CHANNEL_INDEX: Flag<Channel> = {
  ...POSITION_CHANNEL_INDEX,
  ...POLAR_POSITION_CHANNEL_INDEX,

  ...GEO_POSIITON_CHANNEL_INDEX,
  xOffset: 1,
  yOffset: 1,

  // color
  color: 1,
  fill: 1,
  stroke: 1,

  // time
  time: 1,

  // other non-position with scale
  opacity: 1,
  fillOpacity: 1,
  strokeOpacity: 1,

  strokeWidth: 1,
  strokeDash: 1,
  size: 1,
  angle: 1,
  shape: 1,

  // channels without scales
  order: 1,
  text: 1,
  detail: 1,
  key: 1,
  tooltip: 1,
  href: 1,
  url: 1,
  description: 1,
};

export type ColorChannel = 'color' | 'fill' | 'stroke';

export function isColorChannel(channel: Channel): channel is ColorChannel {
  return channel === COLOR || channel === FILL || channel === STROKE;
}

export type FacetChannel = keyof EncodingFacetMapping<any, any>;

const FACET_CHANNEL_INDEX: Flag<keyof EncodingFacetMapping<any, any>> = {
  row: 1,
  column: 1,
  facet: 1,
};

export const FACET_CHANNELS = keys(FACET_CHANNEL_INDEX);

const CHANNEL_INDEX = {
  ...UNIT_CHANNEL_INDEX,
  ...FACET_CHANNEL_INDEX,
};

export const CHANNELS = keys(CHANNEL_INDEX);

const {order: _o, detail: _d, tooltip: _tt1, ...SINGLE_DEF_CHANNEL_INDEX} = CHANNEL_INDEX;
const {row: _r, column: _c, facet: _f, ...SINGLE_DEF_UNIT_CHANNEL_INDEX} = SINGLE_DEF_CHANNEL_INDEX;
/**
 * Channels that cannot have an array of channelDef.
 * model.fieldDef, getFieldDef only work for these channels.
 *
 * (The only two channels that can have an array of channelDefs are "detail" and "order".
 * Since there can be multiple fieldDefs for detail and order, getFieldDef/model.fieldDef
 * are not applicable for them. Similarly, selection projection won't work with "detail" and "order".)
 */

export const SINGLE_DEF_CHANNELS = keys(SINGLE_DEF_CHANNEL_INDEX);

export type SingleDefChannel = (typeof SINGLE_DEF_CHANNELS)[number];

export const SINGLE_DEF_UNIT_CHANNELS = keys(SINGLE_DEF_UNIT_CHANNEL_INDEX);

export type SingleDefUnitChannel = (typeof SINGLE_DEF_UNIT_CHANNELS)[number];

export function isSingleDefUnitChannel(str: string): str is SingleDefUnitChannel {
  return hasOwnProperty(SINGLE_DEF_UNIT_CHANNEL_INDEX, str);
}

export function isChannel(str: string): str is Channel {
  return hasOwnProperty(CHANNEL_INDEX, str);
}

export type SecondaryRangeChannel = 'x2' | 'y2' | 'latitude2' | 'longitude2' | 'theta2' | 'radius2';

export const SECONDARY_RANGE_CHANNEL: SecondaryRangeChannel[] = [X2, Y2, LATITUDE2, LONGITUDE2, THETA2, RADIUS2];

export function isSecondaryRangeChannel(c: ExtendedChannel): c is SecondaryRangeChannel {
  const main = getMainRangeChannel(c);
  return main !== c;
}

export type MainChannelOf<C extends ExtendedChannel> = C extends 'x2'
  ? 'x'
  : C extends 'y2'
    ? 'y'
    : C extends 'latitude2'
      ? 'latitude'
      : C extends 'longitude2'
        ? 'longitude'
        : C extends 'theta2'
          ? 'theta'
          : C extends 'radius2'
            ? 'radius'
            : C;

/**
 * Get the main channel for a range channel. E.g. `x` for `x2`.
 */
export function getMainRangeChannel<C extends ExtendedChannel>(channel: C): MainChannelOf<C> {
  switch (channel) {
    case X2:
      return X as MainChannelOf<C>;
    case Y2:
      return Y as MainChannelOf<C>;
    case LATITUDE2:
      return LATITUDE as MainChannelOf<C>;
    case LONGITUDE2:
      return LONGITUDE as MainChannelOf<C>;
    case THETA2:
      return THETA as MainChannelOf<C>;
    case RADIUS2:
      return RADIUS as MainChannelOf<C>;
  }
  return channel as MainChannelOf<C>;
}

export type SecondaryChannelOf<C extends Channel> = C extends 'x'
  ? 'x2'
  : C extends 'y'
    ? 'y2'
    : C extends 'latitude'
      ? 'latitude2'
      : C extends 'longitude'
        ? 'longitude2'
        : C extends 'theta'
          ? 'theta2'
          : C extends 'radius'
            ? 'radius2'
            : undefined;

export function getVgPositionChannel(channel: PolarPositionChannel | PositionChannel) {
  if (isPolarPositionChannel(channel)) {
    switch (channel) {
      case THETA:
        return 'startAngle';
      case THETA2:
        return 'endAngle';
      case RADIUS:
        return 'outerRadius';
      case RADIUS2:
        return 'innerRadius';
    }
  }
  return channel;
}

/**
 * Get the main channel for a range channel. E.g. `x` for `x2`.
 */
export function getSecondaryRangeChannel<C extends Channel>(channel: C): SecondaryChannelOf<C> | undefined {
  switch (channel) {
    case X:
      return X2 as SecondaryChannelOf<C>;
    case Y:
      return Y2 as SecondaryChannelOf<C>;
    case LATITUDE:
      return LATITUDE2 as SecondaryChannelOf<C>;
    case LONGITUDE:
      return LONGITUDE2 as SecondaryChannelOf<C>;
    case THETA:
      return THETA2 as SecondaryChannelOf<C>;
    case RADIUS:
      return RADIUS2 as SecondaryChannelOf<C>;
  }
  return undefined;
}

export function getSizeChannel(channel: PositionChannel): 'width' | 'height';
export function getSizeChannel(channel: Channel): 'width' | 'height' | undefined;
export function getSizeChannel(channel: Channel): 'width' | 'height' | undefined {
  switch (channel) {
    case X:
    case X2:
      return 'width';
    case Y:
    case Y2:
      return 'height';
  }
  return undefined;
}

/**
 * Get the main channel for a range channel. E.g. `x` for `x2`.
 */
export function getOffsetChannel(channel: Channel) {
  switch (channel) {
    case X:
      return 'xOffset';
    case Y:
      return 'yOffset';
    case X2:
      return 'x2Offset';
    case Y2:
      return 'y2Offset';
    case THETA:
      return 'thetaOffset';
    case RADIUS:
      return 'radiusOffset';
    case THETA2:
      return 'theta2Offset';
    case RADIUS2:
      return 'radius2Offset';
  }
  return undefined;
}

/**
 * Get the main channel for a range channel. E.g. `x` for `x2`.
 */
export function getOffsetScaleChannel(channel: Channel): OffsetScaleChannel {
  switch (channel) {
    case X:
      return 'xOffset';
    case Y:
      return 'yOffset';
  }
  return undefined;
}

export function getMainChannelFromOffsetChannel(channel: OffsetScaleChannel): PositionScaleChannel {
  switch (channel) {
    case 'xOffset':
      return 'x';
    case 'yOffset':
      return 'y';
  }
}

// CHANNELS without COLUMN, ROW
export const UNIT_CHANNELS = keys(UNIT_CHANNEL_INDEX);

// NONPOSITION_CHANNELS = UNIT_CHANNELS without X, Y, X2, Y2;
const {
  x: _x,
  y: _y,
  // x2 and y2 share the same scale as x and y
  x2: _x2,
  y2: _y2,
  //
  xOffset: _xo,
  yOffset: _yo,
  latitude: _latitude,
  longitude: _longitude,
  latitude2: _latitude2,
  longitude2: _longitude2,
  theta: _theta,
  theta2: _theta2,
  radius: _radius,
  radius2: _radius2,
  // The rest of unit channels then have scale
  ...NONPOSITION_CHANNEL_INDEX
} = UNIT_CHANNEL_INDEX;

export const NONPOSITION_CHANNELS = keys(NONPOSITION_CHANNEL_INDEX);
export type NonPositionChannel = (typeof NONPOSITION_CHANNELS)[number];

const POSITION_SCALE_CHANNEL_INDEX = {
  x: 1,
  y: 1,
} as const;
export const POSITION_SCALE_CHANNELS = keys(POSITION_SCALE_CHANNEL_INDEX);
export type PositionScaleChannel = keyof typeof POSITION_SCALE_CHANNEL_INDEX;

export function isXorY(channel: ExtendedChannel): channel is PositionScaleChannel {
  return hasOwnProperty(POSITION_SCALE_CHANNEL_INDEX, channel);
}

export const POLAR_POSITION_SCALE_CHANNEL_INDEX = {
  theta: 1,
  radius: 1,
} as const;

export const POLAR_POSITION_SCALE_CHANNELS = keys(POLAR_POSITION_SCALE_CHANNEL_INDEX);
export type PolarPositionScaleChannel = keyof typeof POLAR_POSITION_SCALE_CHANNEL_INDEX;

export function getPositionScaleChannel(sizeType: 'width' | 'height'): PositionScaleChannel {
  return sizeType === 'width' ? X : Y;
}

const OFFSET_SCALE_CHANNEL_INDEX: {xOffset: 1; yOffset: 1} = {xOffset: 1, yOffset: 1};

export const OFFSET_SCALE_CHANNELS = keys(OFFSET_SCALE_CHANNEL_INDEX);

export type OffsetScaleChannel = (typeof OFFSET_SCALE_CHANNELS)[0];

export function isXorYOffset(channel: Channel): channel is OffsetScaleChannel {
  return hasOwnProperty(OFFSET_SCALE_CHANNEL_INDEX, channel);
}

const TIME_SCALE_CHANNEL_INDEX = {
  time: 1,
} as const;
export const TIME_SCALE_CHANNELS = keys(TIME_SCALE_CHANNEL_INDEX);
export type TimeScaleChannel = keyof typeof TIME_SCALE_CHANNEL_INDEX;

export function isTime(channel: ExtendedChannel): channel is TimeScaleChannel {
  return channel in TIME_SCALE_CHANNEL_INDEX;
}

// NON_POSITION_SCALE_CHANNEL = SCALE_CHANNELS without position / offset
const {
  // x2 and y2 share the same scale as x and y
  // text and tooltip have format instead of scale,
  // href has neither format, nor scale
  text: _t,
  tooltip: _tt,
  href: _hr,
  url: _u,
  description: _al,
  // detail and order have no scale
  detail: _dd,
  key: _k,
  order: _oo,
  ...NONPOSITION_SCALE_CHANNEL_INDEX
} = NONPOSITION_CHANNEL_INDEX;
export const NONPOSITION_SCALE_CHANNELS = keys(NONPOSITION_SCALE_CHANNEL_INDEX);
export type NonPositionScaleChannel = (typeof NONPOSITION_SCALE_CHANNELS)[number];

export function isNonPositionScaleChannel(channel: Channel): channel is NonPositionScaleChannel {
  return hasOwnProperty(NONPOSITION_CHANNEL_INDEX, channel);
}

/**
 * @returns whether Vega supports legends for a particular channel
 */
export function supportLegend(channel: NonPositionScaleChannel) {
  switch (channel) {
    case COLOR:
    case FILL:
    case STROKE:
    case SIZE:
    case SHAPE:
    case OPACITY:
    case STROKEWIDTH:
    case STROKEDASH:
      return true;
    case FILLOPACITY:
    case STROKEOPACITY:
    case ANGLE:
    case TIME:
      return false;
  }
}

// Declare SCALE_CHANNEL_INDEX
const SCALE_CHANNEL_INDEX = {
  ...POSITION_SCALE_CHANNEL_INDEX,
  ...POLAR_POSITION_SCALE_CHANNEL_INDEX,
  ...OFFSET_SCALE_CHANNEL_INDEX,
  ...NONPOSITION_SCALE_CHANNEL_INDEX,
};

/** List of channels with scales */
export const SCALE_CHANNELS = keys(SCALE_CHANNEL_INDEX);
export type ScaleChannel = (typeof SCALE_CHANNELS)[number];

export function isScaleChannel(channel: ExtendedChannel): channel is ScaleChannel {
  return hasOwnProperty(SCALE_CHANNEL_INDEX, channel);
}

export type SupportedMark = Partial<Record<Mark, 'always' | 'binned'>>;

/**
 * Return whether a channel supports a particular mark type.
 * @param channel  channel name
 * @param mark the mark type
 * @return whether the mark supports the channel
 */
export function supportMark(channel: ExtendedChannel, mark: Mark) {
  return getSupportedMark(channel)[mark];
}

const ALL_MARKS: Record<Mark, 'always'> = {
  // all marks
  arc: 'always',
  area: 'always',
  bar: 'always',
  circle: 'always',
  geoshape: 'always',
  image: 'always',
  line: 'always',
  rule: 'always',
  point: 'always',
  rect: 'always',
  square: 'always',
  trail: 'always',
  text: 'always',
  tick: 'always',
};

const {geoshape: _g, ...ALL_MARKS_EXCEPT_GEOSHAPE} = ALL_MARKS;

/**
 * Return a dictionary showing whether a channel supports mark type.
 * @param channel
 * @return A dictionary mapping mark types to 'always', 'binned', or undefined
 */
function getSupportedMark(channel: ExtendedChannel): SupportedMark {
  switch (channel) {
    case COLOR:
    case FILL:
    case STROKE:
    // falls through

    case DESCRIPTION:
    case DETAIL:
    case KEY:
    case TOOLTIP:
    case HREF:
    case ORDER: // TODO: revise (order might not support rect, which is not stackable?)
    case OPACITY:
    case FILLOPACITY:
    case STROKEOPACITY:
    case STROKEWIDTH:

    // falls through

    case FACET:
    case ROW: // falls through
    case COLUMN:
      return ALL_MARKS;
    case X:
    case Y:
    case XOFFSET:
    case YOFFSET:
    case LATITUDE:
    case LONGITUDE:
    case TIME:
      // all marks except geoshape. geoshape does not use X, Y -- it uses a projection
      return ALL_MARKS_EXCEPT_GEOSHAPE;
    case X2:
    case Y2:
    case LATITUDE2:
    case LONGITUDE2:
      return {
        area: 'always',
        bar: 'always',
        image: 'always',
        rect: 'always',
        rule: 'always',
        circle: 'binned',
        point: 'binned',
        square: 'binned',
        tick: 'binned',
        line: 'binned',
        trail: 'binned',
      };
    case SIZE:
      return {
        point: 'always',
        tick: 'always',
        rule: 'always',
        circle: 'always',
        square: 'always',
        bar: 'always',
        text: 'always',
        line: 'always',
        trail: 'always',
      };
    case STROKEDASH:
      return {
        line: 'always',
        point: 'always',
        tick: 'always',
        rule: 'always',
        circle: 'always',
        square: 'always',
        bar: 'always',
        geoshape: 'always',
      };
    case SHAPE:
      return {point: 'always', geoshape: 'always'};
    case TEXT:
      return {text: 'always'};
    case ANGLE:
      return {point: 'always', square: 'always', text: 'always'};
    case URL:
      return {image: 'always'};
    case THETA:
      return {text: 'always', arc: 'always'};
    case RADIUS:
      return {text: 'always', arc: 'always'};
    case THETA2:
    case RADIUS2:
      return {arc: 'always'};
  }
}

export function rangeType(channel: ExtendedChannel): RangeType {
  switch (channel) {
    case X:
    case Y:
    case THETA:
    case RADIUS:
    case XOFFSET:
    case YOFFSET:
    case SIZE:
    case ANGLE:
    case STROKEWIDTH:
    case OPACITY:
    case FILLOPACITY:
    case STROKEOPACITY:
    case TIME:

    // X2 and Y2 use X and Y scales, so they similarly have continuous range. [falls through]
    case X2:
    case Y2:
    case THETA2:
    case RADIUS2:
      return undefined;

    case FACET:
    case ROW:
    case COLUMN:
    case SHAPE:
    case STROKEDASH:
    // TEXT, TOOLTIP, URL, and HREF have no scale but have discrete output [falls through]
    case TEXT:
    case TOOLTIP:
    case HREF:
    case URL:
    case DESCRIPTION:
      return 'discrete';

    // Color can be either continuous or discrete, depending on scale type.
    case COLOR:
    case FILL:
    case STROKE:
      return 'flexible';

    // No scale, no range type.

    case LATITUDE:
    case LONGITUDE:
    case LATITUDE2:
    case LONGITUDE2:
    case DETAIL:
    case KEY:
    case ORDER:
      return undefined;
  }
}
