// Problems in current architecture:
// 1. After loading, can't tell what user actually set versus what came from e.g. GetCapabilities.
//  Solution: layering
// 2. CkanCatalogItem producing a WebMapServiceCatalogItem on load
// 3. Observable spaghetti
//  Solution: think in terms of pipelines with computed observables, document patterns.
// 4. All code for all catalog item types needs to be loaded before we can do anything.
import { FeatureCollection } from "geojson";
import i18next from "i18next";
import { computed, makeObservable, override, runInAction } from "mobx";
import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3";
import Cartographic from "terriajs-cesium/Source/Core/Cartographic";
import GeographicProjection from "terriajs-cesium/Source/Core/GeographicProjection";
import GeographicTilingScheme from "terriajs-cesium/Source/Core/GeographicTilingScheme";
import JulianDate from "terriajs-cesium/Source/Core/JulianDate";
import MapProjection from "terriajs-cesium/Source/Core/MapProjection";
import WebMercatorTilingScheme from "terriajs-cesium/Source/Core/WebMercatorTilingScheme";
import combine from "terriajs-cesium/Source/Core/combine";
import GetFeatureInfoFormat from "terriajs-cesium/Source/Scene/GetFeatureInfoFormat";
import ImageryLayerFeatureInfo from "terriajs-cesium/Source/Scene/ImageryLayerFeatureInfo";
import WebMapServiceImageryProvider from "terriajs-cesium/Source/Scene/WebMapServiceImageryProvider";
import URI from "urijs";
import { JsonObject } from "../../../Core/Json";
import TerriaError from "../../../Core/TerriaError";
import createTransformerAllowUndefined from "../../../Core/createTransformerAllowUndefined";
import filterOutUndefined from "../../../Core/filterOutUndefined";
import isDefined from "../../../Core/isDefined";
import CatalogMemberMixin, {
  getName
} from "../../../ModelMixins/CatalogMemberMixin";
import DiffableMixin, { DiffStratum } from "../../../ModelMixins/DiffableMixin";
import ExportWebCoverageServiceMixin from "../../../ModelMixins/ExportWebCoverageServiceMixin";
import GetCapabilitiesMixin from "../../../ModelMixins/GetCapabilitiesMixin";
import MappableMixin, {
  ImageryParts
} from "../../../ModelMixins/MappableMixin";
import MinMaxLevelMixin from "../../../ModelMixins/MinMaxLevelMixin";
import TileErrorHandlerMixin from "../../../ModelMixins/TileErrorHandlerMixin";
import UrlMixin from "../../../ModelMixins/UrlMixin";
import {
  TimeSeriesFeatureInfoContext,
  csvFeatureInfoContext
} from "../../../Table/tableFeatureInfoContext";
import WebMapServiceCatalogItemTraits, {
  SUPPORTED_CRS_3857,
  SUPPORTED_CRS_4326
} from "../../../Traits/TraitsClasses/WebMapServiceCatalogItemTraits";
import CommonStrata from "../../Definition/CommonStrata";
import CreateModel from "../../Definition/CreateModel";
import LoadableStratum from "../../Definition/LoadableStratum";
import { BaseModel } from "../../Definition/Model";
import StratumOrder from "../../Definition/StratumOrder";
import TerriaFeature from "../../Feature/Feature";
import FeatureInfoContext from "../../Feature/FeatureInfoContext";
import SelectableDimensions, {
  SelectableDimensionEnum
} from "../../SelectableDimensions/SelectableDimensions";
import Terria from "../../Terria";
import proxyCatalogItemUrl from "../proxyCatalogItemUrl";
import WebMapServiceCapabilities from "./WebMapServiceCapabilities";
import WebMapServiceCapabilitiesStratum from "./WebMapServiceCapabilitiesStratum";
import WebMapServiceCatalogGroup from "./WebMapServiceCatalogGroup";

// Remove problematic query parameters from URLs (GetCapabilities, GetMap, ...) - these are handled separately
const QUERY_PARAMETERS_TO_REMOVE = [
  "request",
  "service",
  "x",
  "y",
  "width",
  "height",
  "bbox",
  "layers",
  "styles",
  "version",
  "format",
  "srs",
  "crs"
];

/** This LoadableStratum is responsible for setting WMS version based on CatalogItem.url */
export class WebMapServiceUrlStratum extends LoadableStratum(
  WebMapServiceCatalogItemTraits
) {
  static stratumName = "wms-url-stratum";
  constructor(readonly catalogItem: WebMapServiceCatalogItem) {
    super();
    makeObservable(this);
  }

  duplicateLoadableStratum(model: BaseModel): this {
    return new WebMapServiceUrlStratum(
      model as WebMapServiceCatalogItem
    ) as this;
  }

  @computed get useWmsVersion130() {
    if (
      this.catalogItem.url?.toLowerCase().includes("version=1.1.0") ||
      this.catalogItem.url?.toLowerCase().includes("version=1.1.1")
    ) {
      return false;
    }
  }
}

// Order is important so that the traits are overridden correctly
StratumOrder.addLoadStratum(WebMapServiceUrlStratum.stratumName);
StratumOrder.addLoadStratum(DiffStratum.stratumName);

class WebMapServiceCatalogItem
  extends TileErrorHandlerMixin(
    ExportWebCoverageServiceMixin(
      DiffableMixin(
        MinMaxLevelMixin(
          GetCapabilitiesMixin(
            UrlMixin(
              MappableMixin(
                CatalogMemberMixin(CreateModel(WebMapServiceCatalogItemTraits))
              )
            )
          )
        )
      )
    )
  )
  implements SelectableDimensions, FeatureInfoContext
{
  /**
   * The collection of strings that indicate an Abstract property should be ignored.  If these strings occur anywhere
   * in the Abstract, the Abstract will not be used.  This makes it easy to filter out placeholder data like
   * Geoserver's "A compliant implementation of WMS..." stock abstract.
   */
  static abstractsToIgnore = ["A compliant implementation of WMS"];

  // hide elements in the info section which might show information about the datasource
  _sourceInfoItemNames = [
    i18next.t("models.webMapServiceCatalogItem.getCapabilitiesUrl")
  ];

  _webMapServiceCatalogGroup: undefined | WebMapServiceCatalogGroup = undefined;

  /** Default WMS parameters for version=1.3.0 */
  static defaultParameters130 = {
    transparent: true,
    format: "image/png",
    exceptions: "XML",
    styles: "",
    version: "1.3.0"
  };

  static defaultGetFeatureParameters130 = {
    exceptions: "XML",
    version: "1.3.0"
  };

  /** Default WMS parameters for version=1.1.1 */
  static defaultParameters111 = {
    transparent: true,
    format: "image/png",
    exceptions: "application/vnd.ogc.se_xml",
    styles: "",
    tiled: true,
    version: "1.1.1"
  };

  static defaultGetFeatureParameters111 = {
    exceptions: "application/vnd.ogc.se_xml",
    version: "1.1.1"
  };

  static readonly type = "wms";

  constructor(
    id: string | undefined,
    terria: Terria,
    sourceReference?: BaseModel | undefined
  ) {
    super(id, terria, sourceReference);
    makeObservable(this);
    this.strata.set(
      WebMapServiceUrlStratum.stratumName,
      new WebMapServiceUrlStratum(this)
    );
  }

  get type() {
    return WebMapServiceCatalogItem.type;
  }

  @override
  get shortReport(): string | undefined {
    if (
      this.tilingScheme instanceof GeographicTilingScheme &&
      this.terria.currentViewer.type === "Leaflet"
    ) {
      return i18next.t("map.cesium.notWebMercatorTilingScheme", this);
    }
    return super.shortReport;
  }

  @computed
  get colorScaleRange(): string | undefined {
    if (this.supportsColorScaleRange) {
      return `${this.colorScaleMinimum},${this.colorScaleMaximum}`;
    }
    return undefined;
  }

  async createGetCapabilitiesStratumFromParent(
    capabilities: WebMapServiceCapabilities
  ) {
    const stratum = await WebMapServiceCapabilitiesStratum.load(
      this,
      capabilities
    );
    runInAction(() => {
      this.strata.set(GetCapabilitiesMixin.getCapabilitiesStratumName, stratum);
    });
  }

  protected async forceLoadMapItems(): Promise<void> {
    if (this.invalidLayers.length > 0)
      throw new TerriaError({
        sender: this,
        title: i18next.t("models.webMapServiceCatalogItem.noLayerFoundTitle"),
        message: i18next.t(
          "models.webMapServiceCatalogItem.noLayerFoundMessage",
          { name: getName(this), layers: this.invalidLayers.join(", ") }
        )
      });
  }

  protected async forceLoadMetadata(): Promise<void> {
    if (
      this.strata.get(GetCapabilitiesMixin.getCapabilitiesStratumName) !==
      undefined
    )
      return;
    const stratum = await WebMapServiceCapabilitiesStratum.load(this);
    runInAction(() => {
      this.strata.set(GetCapabilitiesMixin.getCapabilitiesStratumName, stratum);
    });
  }

  @override
  get cacheDuration(): string {
    if (isDefined(super.cacheDuration)) {
      return super.cacheDuration;
    }
    return "0d";
  }

  @computed
  get layersArray(): ReadonlyArray<string> {
    if (Array.isArray(this.layers)) {
      return this.layers;
    } else if (this.layers) {
      return this.layers.split(",");
    } else {
      return [];
    }
  }

  /** LAYERS which are valid (i.e. exist in GetCapabilities).
   * These can be fetched from the server (eg GetMap request)
   */
  @computed get validLayers() {
    const gcStratum: WebMapServiceCapabilitiesStratum | undefined =
      this.strata.get(
        GetCapabilitiesMixin.getCapabilitiesStratumName
      ) as WebMapServiceCapabilitiesStratum;

    if (gcStratum)
      return this.layersArray
        .map((layer) => gcStratum.capabilities.findLayer(layer)?.Name)
        .filter(isDefined);

    return [];
  }

  /** LAYERS which are **INVALID** - they do **not** exist in GetCapabilities
   * These layers can **not** be fetched the server (eg GetMap request)
   */
  @computed get invalidLayers() {
    const gcStratum: WebMapServiceCapabilitiesStratum | undefined =
      this.strata.get(
        GetCapabilitiesMixin.getCapabilitiesStratumName
      ) as WebMapServiceCapabilitiesStratum;

    if (gcStratum)
      return this.layersArray.filter(
        (layer) => !isDefined(gcStratum.capabilities.findLayer(layer)?.Name)
      );

    return [];
  }

  @computed
  get stylesArray(): ReadonlyArray<string> {
    return this.styles?.split(",") ?? [];
  }

  @computed
  get discreteTimes() {
    const getCapabilitiesStratum: WebMapServiceCapabilitiesStratum | undefined =
      this.strata.get(
        GetCapabilitiesMixin.getCapabilitiesStratumName
      ) as WebMapServiceCapabilitiesStratum;
    return getCapabilitiesStratum?.discreteTimes;
  }

  protected get defaultGetCapabilitiesUrl(): string | undefined {
    if (this.uri) {
      const baseUrl = QUERY_PARAMETERS_TO_REMOVE.reduce(
        (url, parameter) =>
          url
            .removeQuery(parameter)
            .removeQuery(parameter.toUpperCase())
            .removeQuery(parameter.toLowerCase()),
        this.uri.clone()
      );

      return baseUrl
        .setSearch({
          service: "WMS",
          version: this.useWmsVersion130 ? "1.3.0" : "1.1.1",
          request: "GetCapabilities"
        })
        .toString();
    } else {
      return undefined;
    }
  }

  @computed
  get canDiffImages(): boolean {
    const hasValidDiffStyles = this.availableDiffStyles.some((diffStyle) =>
      this.styleSelectableDimensions?.[0]?.options?.find(
        (style) => style.id === diffStyle
      )
    );
    return hasValidDiffStyles === true;
  }

  showDiffImage(
    firstDate: JulianDate,
    secondDate: JulianDate,
    diffStyleId: string
  ) {
    if (this.canDiffImages === false) {
      return;
    }

    // A helper to get the diff tag given a date string
    const firstDateStr = this.getTagForTime(firstDate);
    const secondDateStr = this.getTagForTime(secondDate);
    this.setTrait(CommonStrata.user, "firstDiffDate", firstDateStr);
    this.setTrait(CommonStrata.user, "secondDiffDate", secondDateStr);
    this.setTrait(CommonStrata.user, "diffStyleId", diffStyleId);
    this.setTrait(CommonStrata.user, "isShowingDiff", true);
  }

  clearDiffImage() {
    this.setTrait(CommonStrata.user, "firstDiffDate", undefined);
    this.setTrait(CommonStrata.user, "secondDiffDate", undefined);
    this.setTrait(CommonStrata.user, "diffStyleId", undefined);
    this.setTrait(CommonStrata.user, "isShowingDiff", false);
  }

  getLegendBaseUrl(): string {
    // Remove problematic query parameters from URL
    const baseUrl = QUERY_PARAMETERS_TO_REMOVE.reduce(
      (url, parameter) =>
        url
          .removeQuery(parameter)
          .removeQuery(parameter.toUpperCase())
          .removeQuery(parameter.toLowerCase()),
      new URI(this.url)
    );

    return baseUrl.toString();
  }

  getLegendUrlForStyle(
    styleId: string,
    firstDate?: JulianDate,
    secondDate?: JulianDate
  ) {
    const firstTag = firstDate && this.getTagForTime(firstDate);
    const secondTag = secondDate && this.getTagForTime(secondDate);
    const time = filterOutUndefined([firstTag, secondTag]).join(",");
    const layerName = this.availableStyles.find((style) =>
      style.styles.some((s) => s.name === styleId)
    )?.layerName;
    const uri = URI(
      `${this.url}?service=WMS&version=1.1.0&request=GetLegendGraphic&format=image/png&transparent=True`
    )
      .addQuery("layer", encodeURIComponent(layerName || ""))
      .addQuery("styles", encodeURIComponent(styleId));
    if (time) {
      uri.addQuery("time", time);
    }
    return uri.toString();
  }

  @computed
  get mapItems() {
    // Don't return anything if there are invalid layers
    // See forceLoadMapItems for error message
    if (this.invalidLayers.length > 0) return [];

    if (this.isShowingDiff === true) {
      return this._diffImageryParts ? [this._diffImageryParts] : [];
    }

    const result = [];

    const current = this._currentImageryParts;
    if (current) {
      result.push(current);
    }

    const next = this._nextImageryParts;
    if (next) {
      result.push(next);
    }

    return result;
  }

  @computed
  get tilingScheme() {
    if (this.crs) {
      if (SUPPORTED_CRS_3857.includes(this.crs))
        return new WebMercatorTilingScheme();
      if (SUPPORTED_CRS_4326.includes(this.crs))
        return new GeographicTilingScheme();
    }

    return new WebMercatorTilingScheme();
  }

  @computed
  private get _currentImageryParts(): ImageryParts | undefined {
    const imageryProvider = this._createImageryProvider(
      this.currentDiscreteTimeTag
    );
    if (imageryProvider === undefined) {
      return undefined;
    }

    // Reset feature picking for the current imagery layer.
    // We disable feature picking for the next imagery layer.
    imageryProvider.enablePickFeatures = this.allowFeaturePicking;

    return {
      imageryProvider,
      alpha: this.opacity,
      show: this.show,
      clippingRectangle: this.clipToRectangle ? this.cesiumRectangle : undefined
    };
  }

  @computed
  private get _nextImageryParts(): ImageryParts | undefined {
    if (
      this.terria.timelineStack.contains(this) &&
      !this.isPaused &&
      this.nextDiscreteTimeTag
    ) {
      const imageryProvider = this._createImageryProvider(
        this.nextDiscreteTimeTag
      );
      if (imageryProvider === undefined) {
        return undefined;
      }

      // Disable feature picking for the next imagery layer.
      imageryProvider.enablePickFeatures = false;

      return {
        imageryProvider,
        alpha: 0.0,
        show: true,
        clippingRectangle: this.clipToRectangle
          ? this.cesiumRectangle
          : undefined
      };
    } else {
      return undefined;
    }
  }

  @computed
  private get _diffImageryParts(): ImageryParts | undefined {
    const diffStyleId = this.diffStyleId;
    if (
      this.firstDiffDate === undefined ||
      this.secondDiffDate === undefined ||
      diffStyleId === undefined
    ) {
      return;
    }
    const time = `${this.firstDiffDate},${this.secondDiffDate}`;
    const imageryProvider = this._createImageryProvider(time);
    if (imageryProvider) {
      return {
        imageryProvider,
        alpha: this.opacity,
        show: this.show,
        clippingRectangle: this.clipToRectangle
          ? this.cesiumRectangle
          : undefined
      };
    }
    return undefined;
  }

  @computed
  get diffModeParameters(): JsonObject {
    return this.isShowingDiff ? { styles: this.diffStyleId } : {};
  }

  @computed
  get diffModeGetFeatureInfoParameters(): JsonObject {
    return this.isShowingDiff ? { styles: this.diffStyleId } : {};
  }

  getTagForTime(date: JulianDate): string | undefined {
    const index = this.getDiscreteTimeIndex(date);
    return index !== undefined
      ? this.discreteTimesAsSortedJulianDates?.[index].tag
      : undefined;
  }

  private _createImageryProvider = createTransformerAllowUndefined(
    (time: string | undefined): WebMapServiceImageryProvider | undefined => {
      // Don't show anything on the map until GetCapabilities finishes loading.
      if (this.isLoadingMetadata) {
        return undefined;
      }
      if (this.url === undefined) {
        return undefined;
      }

      console.log(`Creating new ImageryProvider for time ${time}`);

      // Set dimensionParameters
      const dimensionParameters = formatDimensionsForOws(this.dimensions);
      if (time !== undefined) {
        dimensionParameters.time = time;
      }

      // Construct parameters objects
      // We use slightly different parameters for GetMap and GetFeatureInfo requests
      const parameters: { [key: string]: any } = {
        ...(this.useWmsVersion130
          ? WebMapServiceCatalogItem.defaultParameters130
          : WebMapServiceCatalogItem.defaultParameters111),
        ...this.parameters,
        ...dimensionParameters
      };

      const getFeatureInfoParameters: { [key: string]: any } = {
        ...(this.useWmsVersion130
          ? WebMapServiceCatalogItem.defaultGetFeatureParameters130
          : WebMapServiceCatalogItem.defaultGetFeatureParameters111),
        feature_count:
          1 +
          (this.maximumShownFeatureInfos ??
            this.terria.configParameters.defaultMaximumShownFeatureInfos),
        ...this.parameters,
        // Note order is important here, as getFeatureInfoParameters may override `time` dimension value
        ...dimensionParameters,
        ...this.getFeatureInfoParameters
      };

      if (this.supportsColorScaleRange) {
        parameters.COLORSCALERANGE = this.colorScaleRange;
      }

      // Styles parameter is mandatory (for GetMap and GetFeatureInfo requests), but can be empty string to use default style
      parameters.styles = this.styles ?? "";
      getFeatureInfoParameters.styles = this.styles ?? "";

      Object.assign(parameters, this.diffModeParameters);
      Object.assign(
        getFeatureInfoParameters,
        this.diffModeGetFeatureInfoParameters
      );

      // Remove problematic query parameters from URL - these are handled by the parameters objects

      const baseUrl = QUERY_PARAMETERS_TO_REMOVE.reduce(
        (url, parameter) =>
          url
            .removeQuery(parameter)
            .removeQuery(parameter.toUpperCase())
            .removeQuery(parameter.toLowerCase()),
        new URI(this.url)
      );

      // Set CRS for WMS 1.3.0
      // Set SRS for WMS 1.1.1
      const crs = this.useWmsVersion130 ? this.crs : undefined;
      const srs = this.useWmsVersion130 ? undefined : this.crs;

      const imageryOptions: WebMapServiceImageryProvider.ConstructorOptions = {
        url: proxyCatalogItemUrl(this, baseUrl.toString()),
        layers: this.validLayers.length > 0 ? this.validLayers.join(",") : "",
        parameters,
        crs,
        srs,
        getFeatureInfoParameters,
        getFeatureInfoUrl: this.getFeatureInfoUrl,
        tileWidth: this.tileWidth,
        tileHeight: this.tileHeight,
        tilingScheme: this.tilingScheme,
        maximumLevel: this.getMaximumLevel(true) ?? this.maximumLevel,
        minimumLevel: this.minimumLevel,
        credit: this.attribution
        // Note: we set enablePickFeatures in _currentImageryParts and _nextImageryParts
      };
      imageryOptions.getFeatureInfoFormats = this.getFeatureInfoFormats;

      if (
        imageryOptions.maximumLevel !== undefined &&
        this.hideLayerAfterMinScaleDenominator
      ) {
        // Make Cesium request one extra level so we can tell the user what's happening and return a blank image.
        ++imageryOptions.maximumLevel;
      }

      const imageryProvider = new WebMapServiceImageryProvider(imageryOptions);
      return this.updateRequestImage(imageryProvider);
    }
  );

  @computed
  get styleSelectableDimensions(): SelectableDimensionEnum[] {
    return this.availableStyles.map((layer, layerIndex) => {
      let name = "Styles";

      // If multiple layers -> prepend layer name to name
      if (this.availableStyles.length > 1) {
        // Attempt to get layer title from GetCapabilitiesStratum
        const layerTitle =
          layer.layerName &&
          (
            this.strata.get(
              GetCapabilitiesMixin.getCapabilitiesStratumName
            ) as WebMapServiceCapabilitiesStratum
          ).capabilitiesLayers.get(layer.layerName)?.Title;

        name = `${
          layerTitle || layer.layerName || `Layer ${layerIndex + 1}`
        } styles`;
      }

      const options = filterOutUndefined(
        layer.styles.map(function (s) {
          if (isDefined(s.name)) {
            return {
              name: s.title || s.name || "",
              id: s.name as string
            };
          }
        })
      );

      // Try to set selectedId to value stored in `styles` trait for this `layerIndex`
      // The `styles` parameter is CSV, a style for each layer
      const selectedId = this.styles?.split(",")?.[layerIndex];

      return {
        name,
        id: `${this.uniqueId}-${layer.layerName}-styles`,
        options,
        selectedId,
        setDimensionValue: (
          stratumId: string,
          newStyle: string | undefined
        ) => {
          if (!newStyle) return;
          runInAction(() => {
            const styles = this.styleSelectableDimensions.map(
              (style) => style.selectedId || ""
            );
            styles[layerIndex] = newStyle;
            this.setTrait(stratumId, "styles", styles.join(","));
          });
        },
        // There is no way of finding out default style if no style has been selected :(
        // To use the default style, we just send empty "styles" to WMS server
        // But if the server doesn't support GetLegendGraphic, then we can't request the default legend
        // Therefore - we only add the "Default style" / undefined option if supportsGetLegendGraphic is true
        allowUndefined: this.supportsGetLegendGraphic && options.length > 1,
        undefinedLabel: i18next.t(
          "models.webMapServiceCatalogItem.defaultStyleLabel"
        ),
        disable: this.isShowingDiff
      };
    });
  }

  @computed
  get wmsDimensionSelectableDimensions(): SelectableDimensionEnum[] {
    const dimensions: SelectableDimensionEnum[] = [];

    // For each layer -> For each dimension
    this.availableDimensions.forEach((layer) => {
      layer.dimensions.forEach((dim) => {
        // Only add dimensions if hasn't already been added (multiple layers may have the same dimension)
        if (
          !isDefined(dim.name) ||
          dim.values.length < 2 ||
          dimensions.findIndex((findDim) => findDim.name === dim.name) !== -1
        ) {
          return;
        }

        dimensions.push({
          name: dim.name,
          id: `${this.uniqueId}-${dim.name}`,
          options: dim.values.map((value) => {
            let name = value;
            // Add units and unitSybol if defined
            if (typeof dim.units === "string" && dim.units !== "") {
              if (typeof dim.unitSymbol === "string" && dim.unitSymbol !== "") {
                name = `${value} (${dim.units} ${dim.unitSymbol})`;
              } else {
                name = `${value} (${dim.units})`;
              }
            }
            return {
              name,
              id: value
            };
          }),

          // Set selectedId to value stored in `dimensions` trait, the default value, or the first available value
          selectedId:
            this.dimensions?.[dim.name]?.toString() ||
            dim.default ||
            dim.values[0],

          setDimensionValue: (
            stratumId: string,
            newDimension: string | undefined
          ) => {
            let newDimensions: any = {};

            newDimensions[dim.name!] = newDimension;

            if (isDefined(this.dimensions)) {
              newDimensions = combine(newDimensions, this.dimensions);
            }
            runInAction(() => {
              this.setTrait(stratumId, "dimensions", newDimensions);
            });
          }
        });
      });
    });

    return dimensions;
  }

  @override
  get selectableDimensions() {
    if (this.disableDimensionSelectors) {
      return super.selectableDimensions;
    }

    return filterOutUndefined([
      ...super.selectableDimensions,
      ...this.wmsDimensionSelectableDimensions,
      ...this.styleSelectableDimensions
    ]);
  }

  /** If GetFeatureInfo/GetTimeseries request is returning CSV, we need to parse it into TimeSeriesFeatureInfoContext.
   */
  @computed get featureInfoContext(): (
    feature: TerriaFeature
  ) => TimeSeriesFeatureInfoContext {
    if (this.getFeatureInfoFormat.format !== "text/csv") return () => ({});
    return csvFeatureInfoContext(this);
  }

  @computed
  get getFeatureInfoFormats() {
    const customFormat = this.getFeatureInfoFormat;
    if (customFormat) {
      if (customFormat.type === "json") {
        return [
          new GetFeatureInfoFormat("json", "application/json", (json) =>
            geoJsonToFeatureInfoWithProject(json, this.tilingScheme.projection)
          )
        ];
      } else {
        return [
          new GetFeatureInfoFormat(customFormat.type, customFormat.format)
        ];
      }
    }
    return [
      new GetFeatureInfoFormat("json", "application/json", (json) =>
        geoJsonToFeatureInfoWithProject(json, this.tilingScheme.projection)
      ),
      new GetFeatureInfoFormat("xml", "text/xml"),
      new GetFeatureInfoFormat("csv", "text/csv")
    ];
  }
}

/**
 * Add `_dim` prefix to dimensions for OWS (WMS, WCS...) excluding time, styles and elevation
 */
export function formatDimensionsForOws(
  dimensions: { [key: string]: string } | undefined
) {
  if (!isDefined(dimensions)) {
    return {};
  }
  return Object.entries(dimensions).reduce<{ [key: string]: string }>(
    (formattedDimensions, [key, value]) =>
      // elevation is specified as simply "elevation", styles is specified as "styles"
      // Other (custom) dimensions are prefixed with 'dim_'.
      // See WMS 1.3.0 spec section C.3.2 and C.3.3.
      {
        formattedDimensions[
          ["time", "styles", "elevation"].includes(key?.toLowerCase())
            ? key
            : `dim_${key}`
        ] = value;
        return formattedDimensions;
      },
    {}
  );
}

// Take the GetFeatureInfo response and reproject the feature coordinates to geographic if necessary, so the position is correct when displayed on the map.
// Cesium [GetFeatureInfoFormat](https://github.com/CesiumGS/cesium/blob/5754031f65646bee5f9d0e9a56dec7d3677a8b08/packages/engine/Source/Scene/GetFeatureInfoFormat.js#L74) assumes picked features is returned in geographic projection, so we need to convert them when tile scheme is not geographic.
function geoJsonToFeatureInfoWithProject(
  json: FeatureCollection,
  projection: MapProjection
) {
  const result = [];

  const features = json.features;
  for (let i = 0; i < features.length; ++i) {
    const feature = features[i];

    const featureInfo = new ImageryLayerFeatureInfo();
    featureInfo.data = feature;
    featureInfo.properties = feature.properties;
    featureInfo.configureNameFromProperties(feature.properties);
    featureInfo.configureDescriptionFromProperties(feature.properties);

    // If this is a point feature, use the coordinates of the point.
    if (!!feature.geometry && feature.geometry.type === "Point") {
      const x = feature.geometry.coordinates[0];
      const y = feature.geometry.coordinates[1];

      if (!projection || projection instanceof GeographicProjection) {
        featureInfo.position = Cartographic.fromDegrees(x, y);
      } else {
        const positionInMeters = new Cartesian3(x, y, 0);
        const cartographic = projection.unproject(positionInMeters);
        featureInfo.position = cartographic;
      }
    }

    result.push(featureInfo);
  }

  return result;
}

export default WebMapServiceCatalogItem;
