import ReferenceMixin from "../../../ModelMixins/ReferenceMixin";
import UrlMixin from "../../../ModelMixins/UrlMixin";
import CatalogMemberFactory from "../CatalogMemberFactory";
import CreateModel from "../../Definition/CreateModel";
import { BaseModel } from "../../Definition/Model";
import StratumFromTraits from "../../Definition/StratumFromTraits";
import Terria from "../../Terria";
import ModelTraits from "../../../Traits/ModelTraits";
import UrlReferenceTraits from "../../../Traits/TraitsClasses/UrlReferenceTraits";
import StratumOrder from "../../Definition/StratumOrder";
import CatalogMemberMixin from "../../../ModelMixins/CatalogMemberMixin";
import updateModelFromJson from "../../Definition/updateModelFromJson";

const urlRecordStratum = "url-record";
StratumOrder.addDefaultStratum(urlRecordStratum);

export default class UrlReference extends UrlMixin(
  ReferenceMixin(CreateModel(UrlReferenceTraits))
) {
  static readonly type = "url-reference";

  get type() {
    return UrlReference.type;
  }

  constructor(
    id: string | undefined,
    terria: Terria,
    sourceReference?: BaseModel,
    strata?: Map<string, StratumFromTraits<ModelTraits>>
  ) {
    super(id, terria, sourceReference, strata);
  }

  protected forceLoadReference(
    previousTarget: BaseModel | undefined
  ): Promise<BaseModel | undefined> {
    if (this.url === undefined || this.uniqueId === undefined) {
      return Promise.resolve(undefined);
    }

    const target = UrlReference.createCatalogMemberFromUrlReference(
      this,
      this.uniqueId,
      this.url,
      this.terria,
      this.allowLoad || false
    );

    return Promise.resolve(target);
  }

  private static async createCatalogMemberFromUrlReference(
    sourceReference: BaseModel,
    id: string,
    url: string,
    terria: Terria,
    allowLoad: boolean,
    _index?: number
  ): Promise<BaseModel | undefined> {
    const index = _index || 0;
    if (index >= UrlToCatalogMemberMapping.mapping.length) {
      return Promise.resolve(undefined);
    }

    // Does the mapping at this index match this url?
    // Can we load it if we need to?
    if (
      (UrlToCatalogMemberMapping.mapping[index].matcher &&
        !UrlToCatalogMemberMapping.mapping[index].matcher(url)) ||
      (UrlToCatalogMemberMapping.mapping[index].requiresLoad && !allowLoad)
    ) {
      // Nope, try the mapping at the next index.
      return UrlReference.createCatalogMemberFromUrlReference(
        sourceReference,
        id,
        url,
        terria,
        allowLoad,
        index + 1
      );
    } else {
      // We've got a match! Try and create a model
      const item = CatalogMemberFactory.create(
        UrlToCatalogMemberMapping.mapping[index].type,
        sourceReference.uniqueId,
        terria,
        sourceReference
      );

      if (item === undefined) {
        // Creating the model failed, try the mapping at the next index
        return UrlReference.createCatalogMemberFromUrlReference(
          sourceReference,
          id,
          url,
          terria,
          allowLoad,
          index + 1
        );
      }

      updateModelFromJson(item, urlRecordStratum, {
        name: url,
        url: url
      }).logError();

      if (allowLoad && CatalogMemberMixin.isMixedInto(item)) {
        const loadMetadataResult = await item.loadMetadata();
        if (loadMetadataResult.error) {
          return UrlReference.createCatalogMemberFromUrlReference(
            sourceReference,
            id,
            url,
            terria,
            allowLoad,
            index + 1
          );
        }
      }
      return item;
    }
  }
}

export type Matcher = (input: string) => boolean;

export interface MappingEntry {
  matcher: Matcher;
  type: string;
  requiresLoad: boolean;
}

export class UrlMapping {
  mapping: MappingEntry[] = [];

  register(matcher: Matcher, type: string, requiresLoad?: boolean) {
    this.mapping.push({
      matcher,
      type,
      requiresLoad: Boolean(requiresLoad)
    });
  }
}

export const UrlToCatalogMemberMapping = new UrlMapping();

/**
 * Register a url handler for a specific catalog member type.
 *
 * When a user uploads a url or drags-n-drops a particular file, the matchers
 * are tried in order and when there is a match we try and create a catalog
 * member of that type.
 *
 * @param catalogMemberType The type string identifying the catalog member
 * @param matcher The matcher definition
 * @param requiresLoad Should be set to `true` if in addition to URL matching we must also try and load
 *    the item successfully for it to be a valid match. Eg WMS/WFS groups that require enumeration.
 */
export function registerUrlHandlerForCatalogMemberType(
  catalogMemberType: string,
  matcher: Matcher,
  requiresLoad?: boolean
): void {
  UrlToCatalogMemberMapping.register(matcher, catalogMemberType, requiresLoad);
}
