import { collectGeoJSON, offset, pointsFromLngLat } from "./geometry.js";
import { getFeatureType, listFeatureTypeSchemas } from "./feature-types.js";

export const YARD_SCHEMA_VERSION = 2;

class YardFeature {
  constructor(kind, geometryType, name, details = {}) {
    const featureType = getFeatureType(kind);
    if (featureType.geometryType !== geometryType) {
      throw new Error(
        `${kind} features must use ${featureType.geometryType} geometry, got ${geometryType}`,
      );
    }

    this.kind = kind;
    this.geometryType = geometryType;
    this.name = name ?? featureType.label;
    this.id = buildFeatureId(kind, this.name);
    const { plantedDate = null, removedDate = null, ...restDetails } = details;
    this.plantedDate = plantedDate;
    this.removedDate = removedDate;
    this.details = { ...restDetails };
    this.coordinates = [];
    this.parentId = null;
    this.groupIds = [];
  }

  add(...values) {
    this.coordinates.push(resolveCoordinate(values));
    return this;
  }

  trace(coordinates) {
    for (const coordinate of pointsFromLngLat(coordinates)) {
      this.add(coordinate);
    }

    return this;
  }

  withDetails(details) {
    Object.assign(this.details, details);
    return this;
  }

  withId(id) {
    this.id = id;
    return this;
  }

  plantedOn(plantedDate) {
    this.plantedDate = plantedDate;
    return this;
  }

  removedOn(removedDate) {
    this.removedDate = removedDate;
    return this;
  }

  withLifecycleDates(plantedDate, removedDate = null) {
    this.plantedDate = plantedDate;
    this.removedDate = removedDate;
    return this;
  }

  childOf(parent) {
    this.parentId = resolveFeatureReference(parent);
    return this;
  }

  inGroup(...groups) {
    this.groupIds.push(...groups.map(resolveGroupReference));
    this.groupIds = [...new Set(this.groupIds)];
    return this;
  }

  toGeoJSON() {
    const label = getFeatureType(this.kind).label;
    const properties = {
      id: this.id,
      kind: this.kind,
      label,
      name: this.name,
      plantedDate: this.plantedDate,
      removedDate: this.removedDate,
      parentId: this.parentId,
      groupIds: this.groupIds,
      details: this.details,
    };

    if (this.geometryType === "Point") {
      const [first] = this.coordinates;
      return {
        type: "Feature",
        properties,
        geometry: {
          type: "Point",
          coordinates: [first.lon, first.lat],
        },
      };
    }

    const coordinates = this.coordinates.map((coordinate) => [coordinate.lon, coordinate.lat]);
    const geometry =
      this.geometryType === "LineString"
        ? { type: "LineString", coordinates }
        : { type: "Polygon", coordinates: [closeRing(coordinates)] };

    return {
      type: "Feature",
      properties,
      geometry,
    };
  }
}

function buildFeatureId(kind, name) {
  return `${kind}:${slugify(name)}`;
}

function slugify(value) {
  return String(value)
    .toLowerCase()
    .replaceAll(/[^a-z0-9]+/g, "-")
    .replaceAll(/^-+|-+$/g, "")
    .replaceAll(/-{2,}/g, "-");
}

function resolveFeatureReference(featureOrId) {
  if (typeof featureOrId === "string") {
    return featureOrId;
  }

  if (featureOrId && typeof featureOrId.id === "string") {
    return featureOrId.id;
  }

  throw new Error("Expected a feature or feature id");
}

function resolveGroupReference(groupOrId) {
  if (typeof groupOrId === "string") {
    return groupOrId;
  }

  if (groupOrId && typeof groupOrId.id === "string") {
    return groupOrId.id;
  }

  throw new Error("Expected a group or group id");
}

export function defineGroup(id, label, options = {}) {
  return {
    id,
    label,
    parentGroupId: options.parentGroupId ?? null,
  };
}

class PolygonFeature extends YardFeature {
  constructor(kind, name, details) {
    super(kind, "Polygon", name, details);
  }
}

class LineFeature extends YardFeature {
  constructor(kind, name, details) {
    super(kind, "LineString", name, details);
  }
}

class PointFeature extends YardFeature {
  constructor(kind, name, details) {
    super(kind, "Point", name, details);
  }
}

function closeRing(coordinates) {
  if (coordinates.length === 0) {
    return coordinates;
  }

  const [firstLon, firstLat] = coordinates[0];
  const [lastLon, lastLat] = coordinates[coordinates.length - 1];
  if (firstLon === lastLon && firstLat === lastLat) {
    return coordinates;
  }

  return [...coordinates, coordinates[0]];
}

function isPoint(value) {
  return Boolean(value) && typeof value.lat === "number" && typeof value.lon === "number";
}

function isMove(value) {
  return (
    Boolean(value) &&
    typeof value.northMeters === "number" &&
    typeof value.eastMeters === "number"
  );
}

function isOffsetAnchor(value) {
  return Boolean(value) && value.kind === "offset-anchor" && isPoint(value.origin);
}

function resolveCoordinate(values) {
  if (values.length === 1 && isPoint(values[0])) {
    return values[0];
  }

  const [first, ...rest] = values;
  if (isOffsetAnchor(first) && rest.every(isMove)) {
    if (rest.length === 0) {
      return first.origin;
    }

    return offset(first.origin, ...rest);
  }

  throw new Error("add(...) expects a point, or offset(origin) followed by directional moves");
}

export class LotBoundary extends PolygonFeature {
  constructor(name = "Lot Boundary", details = {}) {
    super("lotBoundary", name, details);
  }
}

export class Fence extends LineFeature {
  constructor(name = "Fence", details = {}) {
    super("fence", name, details);
  }
}

export class YardGridLine extends LineFeature {
  constructor(name = "Yard Grid", details = {}) {
    super("yardGrid", name, details);
  }
}

export class House extends PolygonFeature {
  constructor(name = "House", details = {}) {
    super("house", name, details);
  }
}

export class Garage extends PolygonFeature {
  constructor(name = "Garage", details = {}) {
    super("garage", name, details);
  }
}

export class Porch extends PolygonFeature {
  constructor(name = "Porch", details = {}) {
    super("porch", name, details);
  }
}

export class Deck extends PolygonFeature {
  constructor(name = "Deck", details = {}) {
    super("deck", name, details);
  }
}

export class Walkway extends PolygonFeature {
  constructor(name = "Walkway", details = {}) {
    super("walkway", name, details);
  }
}

export class Driveway extends PolygonFeature {
  constructor(name = "Driveway", details = {}) {
    super("driveway", name, details);
  }
}

export class FlowerBed extends PolygonFeature {
  constructor(name = "Flower Bed", details = {}) {
    super("flowerBed", name, details);
  }
}

export class Plant extends PointFeature {
  constructor(name = "Plant", details = {}, kind = "plant") {
    super(kind, name, details);
  }
}

export class Flower extends Plant {
  constructor(name = "Flower", details = {}, kind = "flower") {
    super(name, details, kind);
  }
}

export class Daylily extends Flower {
  constructor(name = "Daylily", details = {}) {
    super(name, {
      commonName: "Daylily",
      genus: "Hemerocallis",
      ...details,
    }, "daylily");
  }
}

export class Shrub extends Plant {
  constructor(name = "Shrub", details = {}) {
    super(name, details, "shrub");
  }
}

export class Bush extends Shrub {
  constructor(name = "Bush", details = {}) {
    super(name, details);
  }
}

export class Tree extends Plant {
  constructor(name = "Tree", details = {}) {
    super(name, details, "tree");
  }
}

export class Sprinkler extends PointFeature {
  constructor(name = "Sprinkler", details = {}) {
    super("sprinkler", name, details);
  }
}

export function collectYardData(features, groups = []) {
  const collection = collectGeoJSON(features);
  collection.schemaVersion = YARD_SCHEMA_VERSION;
  collection.lifecycleFields = {
    plantedDate: "date",
    removedDate: "date",
  };
  collection.featureTypes = listFeatureTypeSchemas();
  collection.groups = groups.map((group) => ({
    id: group.id,
    label: group.label,
    parentGroupId: group.parentGroupId ?? null,
  }));
  return collection;
}

export { collectGeoJSON };
