import { isDefined } from "../../helpers/misc";
import { GeoChartRegion } from "../../types/chart/geo_chart";
import { ModelConfig } from "../../types/model";
import { EvaluationPlugin, EvaluationPluginConfig } from "../evaluation_plugin";

export class GeoLoaderEvaluation extends EvaluationPlugin {
  static getters = [
    "getGeoJsonFeatures",
    "geoFeatureNameToId",
    "getGeoChartAvailableRegions",
    "loadUsedGeoJsonFeatures",
    "isRegionAvailable",
  ] as const;

  private readonly geoJsonService: ModelConfig["external"]["geoJsonService"];

  private geoJsonCache: { [region: string]: GeoJSON.Feature[] | null | Promise<void> } = {};

  constructor(config: EvaluationPluginConfig) {
    super(config);
    this.geoJsonService = config.external.geoJsonService;
  }

  getGeoChartAvailableRegions(): GeoChartRegion[] {
    if (!this.geoJsonService) {
      console.error("No geoJsonService provided to the model");
      return [];
    }
    return this.geoJsonService.getAvailableRegions() || [];
  }

  getGeoJsonFeatures(region: string): GeoJSON.Feature[] | undefined {
    if (!this.geoJsonService) {
      console.error("No geoJsonService provided to the model");
      return;
    }

    const cachedGeoJson = this.geoJsonCache[region];

    if (cachedGeoJson instanceof Promise) {
      return undefined;
    }
    if (cachedGeoJson !== undefined) {
      return cachedGeoJson ?? undefined;
    }

    this.geoJsonCache[region] = new Promise<void>(async (resolve) => {
      const json = await this.geoJsonService?.getTopoJson(region);
      this.geoJsonCache[region] = this.convertToGeoJson(json);
      this.dispatch("EVALUATE_CHARTS");
      resolve();
    });
    return undefined;
  }

  geoFeatureNameToId(region: string, featureName: string): string | undefined {
    if (!this.geoJsonService) {
      console.error("No geoJsonService provided to the model");
      return;
    }
    return this.geoJsonService.geoFeatureNameToId(region, featureName);
  }

  private convertToGeoJson(
    json: GeoJSON.FeatureCollection | TopoJSON.Topology
  ): GeoJSON.Feature[] | null {
    if (!json) {
      return null;
    }
    // TopoJSON
    if (json.type === "Topology") {
      if (!globalThis.ChartGeo) {
        return null;
      }
      const features = globalThis.ChartGeo.topojson.feature(json, Object.values(json.objects)[0]);
      return features.type === "FeatureCollection" ? features.features : [features];
    }
    // GeoJSON
    else if (json.type === "FeatureCollection") {
      return json.features;
    }

    throw new Error("Invalid TopoJSON");
  }

  async loadUsedGeoJsonFeatures() {
    const regions = this.getters
      .getSheetIds()
      .flatMap((sheetId) =>
        this.getters.getChartIds(sheetId).map((chartId) => {
          const definition = this.getters.getChartDefinition(chartId);
          return definition.type === "geo"
            ? definition.region || this.getters.getGeoChartAvailableRegions()[0]?.id
            : undefined;
        })
      )
      .filter(isDefined);

    const uniqueRegions = Array.from(new Set(regions));
    if (uniqueRegions.length && !this.geoJsonService) {
      console.error("No geoJsonService provided to the model");
      return;
    }

    for (const region of uniqueRegions) {
      this.getGeoJsonFeatures(region); // Trigger the loading of the regions
    }

    const promises = uniqueRegions.map((region) => {
      const cachedGeoJson = this.geoJsonCache[region];
      return cachedGeoJson instanceof Promise ? cachedGeoJson : Promise.resolve();
    });
    await Promise.all(promises);
  }

  isRegionAvailable(region: string): boolean {
    if (!this.geoJsonService) {
      console.error("No geoJsonService provided to the model");
      return false;
    }
    return this.geoJsonService.isRegionAvailable(region);
  }
}
