/**
 * Tin (Triangulated Irregular Network) クラス
 * 2つの平面座標系間の同相変換を実現します。
 */

import {
  booleanPointInPolygon,
  centroid as turfCentroid,
  convex,
  featureCollection,
  point,
  polygon,
} from "@turf/turf";
import type { Feature, Point, Polygon, Position } from "geojson";
import {
  counterTri,
  format_version as FORMAT_VERSION_V2,
  normalizeEdges,
  rotateVerticesTriangle,
  Transform,
  transformArr,
} from "@maplat/transform";

const FORMAT_VERSION_V3 = 3.00000;
import type {
  Compiled,
  CompiledLegacy,
  Edge,
  EdgeSet,
  EdgeSetLegacy,
  PointSet,
  PropertyTriKey,
  StrictMode,
  Tins,
  TinsBD,
  Tri,
  VertexMode,
  YaxisMode,
} from "@maplat/transform";
import constrainedTin from "./constrained-tin.ts";
import {
  calculateBirdeyeVertices,
  calculatePlainVertices,
  type BoundaryVerticesParams,
} from "./boundary-vertices.ts";
import findIntersections from "./kinks.ts";
import { insertSearchIndex } from "./searchutils.ts";
import { counterPoint, createPoint, vertexCalc } from "./vertexutils.ts";
import { buildPointsWeightBuffer } from "./weight-buffer.ts";
import { resolveOverlaps } from "./strict-overlap.ts";
import type { SearchIndex } from "./searchutils.ts";
import type { PointsSetBD, VertexPosition } from "./types/tin.d.ts";

type EdgeSegmentItem = [Position, number, number, number, string?];
type EdgeNodeItem = [Position, Position, number];

/**
 * Tinクラスの初期化オプション
 */
export interface Options {
  bounds?: Position[];
  wh?: number[];
  vertexMode?: VertexMode;
  strictMode?: StrictMode;
  yaxisMode?: YaxisMode;
  importance?: number;
  priority?: number;
  stateFull?: boolean;
  points?: PointSet[];
  edges?: EdgeSet[];
  /** true にすると v2 アルゴリズム（wh bbox・turf 重心・4 境界頂点）で build する */
  useV2Algorithm?: boolean;
}

/**
 * Tin (Triangulated Irregular Network) クラス
 * Transformクラスを拡張し、TINネットワークの生成機能を追加
 */
export class Tin extends Transform {
  importance: number;
  priority: number;
  pointsSet: PointsSetBD | undefined;
  useV2Algorithm: boolean;

  /**
   * Tinクラスのインスタンスを生成します
   * @param options - 初期化オプション
   */
  constructor(options: Options = {}) {
    super();

    if (options.bounds) {
      this.setBounds(options.bounds);
    } else {
      this.setWh(options.wh);
      this.vertexMode = options.vertexMode || Tin.VERTEX_PLAIN;
    }

    this.strictMode = options.strictMode || Tin.MODE_AUTO;
    this.yaxisMode = options.yaxisMode || Tin.YAXIS_INVERT;
    this.importance = options.importance || 0;
    this.priority = options.priority || 0;
    this.stateFull = options.stateFull || false;
    this.useV2Algorithm = options.useV2Algorithm ?? false;

    if (options.points) {
      this.setPoints(options.points);
    }
    if (options.edges) {
      this.setEdges(options.edges);
    }
  }

  /**
   * フォーマットバージョンを取得します
   */
  getFormatVersion(): number {
    return this.useV2Algorithm ? FORMAT_VERSION_V2 : FORMAT_VERSION_V3;
  }

  /**
   * 制御点（GCP: Ground Control Points）を設定します。
   * 指定した点群に合わせて内部のTINキャッシュをリセットします。
   */
  setPoints(points: PointSet[]): void {
    if (this.yaxisMode === Tin.YAXIS_FOLLOW) {
      points = points.map((point) => [
        point[0],
        [point[1][0], -1 * point[1][1]],
      ]);
    }
    this.points = points;
    this.tins = undefined;
    this.indexedTins = undefined;
  }

  /**
   * エッジ（制約線）を設定します。
   * 制約線を正規化した上で、依存するキャッシュをリセットします。
   */
  setEdges(edges: EdgeSet[] | EdgeSetLegacy[] = []): void {
    this.edges = normalizeEdges(edges);
    this.edgeNodes = undefined;
    this.tins = undefined;
    this.indexedTins = undefined;
  }

  /**
   * 境界ポリゴンを設定します
   */
  setBounds(bounds: Position[]): void {
    this.bounds = bounds;

    let minx = bounds[0][0];
    let maxx = minx;
    let miny = bounds[0][1];
    let maxy = miny;
    const coords = [bounds[0]];

    for (let i = 1; i < bounds.length; i++) {
      const bound = bounds[i];
      if (bound[0] < minx) minx = bound[0];
      if (bound[0] > maxx) maxx = bound[0];
      if (bound[1] < miny) miny = bound[1];
      if (bound[1] > maxy) maxy = bound[1];
      coords.push(bound);
    }
    coords.push(bounds[0]);

    this.boundsPolygon = polygon([coords]);
    this.xy = [minx, miny];
    this.wh = [maxx - minx, maxy - miny];
    this.vertexMode = Tin.VERTEX_PLAIN;
    this.tins = undefined;
    this.indexedTins = undefined;
  }

  /**
   * 現在の設定を永続化可能な形式にコンパイルします
   */
  getCompiled(): Compiled {
    const compiled: Compiled = {} as Compiled;
    compiled.version = this.useV2Algorithm ? FORMAT_VERSION_V2 : FORMAT_VERSION_V3;
    compiled.points = this.points;
    compiled.weight_buffer = this.pointsWeightBuffer ?? {};
    compiled.centroid_point = [
      this.centroid!.forw!.geometry!.coordinates,
      this.centroid!.forw!.properties!.target.geom,
    ];
    compiled.vertices_params = [
      this.vertices_params!.forw![0],
      this.vertices_params!.bakw![0],
    ];
    compiled.vertices_points = [];

    const vertices = this.vertices_params!.forw![1];
    if (vertices) {
      for (let i = 0; i < vertices.length; i++) {
        const vertex = vertices[i].features[0];
        const forw = vertex.geometry!.coordinates[0][1];
        const bakw = vertex.properties!.b.geom;
        compiled.vertices_points[i] = [forw, bakw];
      }
    }

    compiled.strict_status = this.strict_status;
    compiled.tins_points = [[]];

    this.tins!.forw!.features.map((tin: Tri) => {
      compiled.tins_points[0].push(
        (["a", "b", "c"] as PropertyTriKey[]).map((key) =>
          tin.properties![key].index
        ),
      );
    });

    if (this.strict_status === Tin.STATUS_LOOSE) {
      compiled.tins_points[1] = [];
      this.tins!.bakw!.features.map((tin: Tri) => {
        compiled.tins_points[1].push(
          (["a", "b", "c"] as PropertyTriKey[]).map((key) =>
            tin.properties![key].index
          ),
        );
      });
    } else if (this.strict_status === Tin.STATUS_ERROR && this.kinks?.bakw) {
      compiled.kinks_points = this.kinks.bakw.features.map(
        (kink) => kink.geometry!.coordinates,
      );
    }

    compiled.yaxisMode = this.yaxisMode;
    compiled.vertexMode = this.vertexMode;
    compiled.strictMode = this.strictMode;

    if (this.bounds) {
      compiled.bounds = this.bounds;
      compiled.boundsPolygon = this.boundsPolygon;
      if (this.useV2Algorithm) {
        // V2 submap: xy/wh define the bbox used for triangulation — must be preserved.
        compiled.xy = this.xy;
        compiled.wh = this.wh;
      }
      // V3 submap: bounds polygon is the envelope; xy/wh are not used for
      // triangulation and are therefore not serialized.
    } else {
      // Main map: wh stores image dimensions for display and V2 bbox calculation.
      compiled.wh = this.wh;
    }

    compiled.edges = this.edges ?? [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    compiled.edgeNodes = (this.edgeNodes ?? []) as any;

    return compiled;
  }

  /**
   * コンパイルされた設定を適用します（v3+フォーマット対応）
   *
   * バージョン3以上のコンパイル済みデータが渡された場合は restoreV3State() を
   * 使用してN頂点対応の復元を行います。それ以外は基底クラスの実装に委譲します。
   */
  override setCompiled(compiled: Compiled | CompiledLegacy): void {
    super.setCompiled(compiled);
  }

  /**
   * 幅と高さを設定します
   */
  setWh(wh?: number[]): void {
    this.wh = wh || [100, 100]; // デフォルト値を設定
    this.xy = [0, 0];
    this.bounds = undefined;
    this.boundsPolygon = undefined;
    this.tins = undefined;
    this.indexedTins = undefined;
  }

  /**
   * 頂点モードを設定します
   */
  setVertexMode(mode: VertexMode): void {
    this.vertexMode = mode;
    this.tins = undefined;
    this.indexedTins = undefined;
  }

  /**
   * 厳密性モードを設定します
   */
  setStrictMode(mode: StrictMode): void {
    this.strictMode = mode;
    this.tins = undefined;
    this.indexedTins = undefined;
  }

  /**
   * 厳密なTINを計算します
   */
  calcurateStrictTin(): void {
    const bakTins = this.tins!.forw!.features.map((tri: Tri) =>
      counterTri(tri)
    );
    this.tins!.bakw = featureCollection(bakTins);

    const searchIndex: SearchIndex = {};
    this.tins!.forw!.features.forEach((forTri: Tri, index: number) => {
      const bakTri = this.tins!.bakw!.features[index];
      insertSearchIndex(searchIndex, { forw: forTri, bakw: bakTri });
    });

    resolveOverlaps(
      this.tins!,
      searchIndex,
      this.pointsSet?.edges || [],
    );

    const kinks = ["forw", "bakw"].map((direction) => {
      const tins = this.tins![direction as keyof TinsBD]!.features.map(
        (tin: Tri) => tin.geometry!.coordinates[0],
      );
      return findIntersections(tins);
    });

    if (kinks[0].length === 0 && kinks[1].length === 0) {
      this.strict_status = Tin.STATUS_STRICT;
      delete this.kinks;
    } else {
      this.strict_status = Tin.STATUS_ERROR;
      this.kinks = {
        forw: featureCollection(kinks[0]),
        bakw: featureCollection(kinks[1]),
      };
    }
  }

  /**
   * 点群セットを生成します。
  * GCP と中間エッジノードを GeoJSON Point に変換し、後続の三角分割に備えます。
   */
  generatePointsSet(): {
    forw: Feature<Point>[];
    bakw: Feature<Point>[];
    edges: Edge[];
  } {
    const pointsSet: { forw: Feature<Point>[]; bakw: Feature<Point>[] } = {
      forw: [],
      bakw: [],
    };

    // Generate points
    for (let i = 0; i < this.points.length; i++) {
      const forw = this.points[i][0];
      const bakw = this.points[i][1];
      const forPoint = createPoint(forw, bakw, i);
      pointsSet.forw.push(forPoint);
      pointsSet.bakw.push(counterPoint(forPoint));
    }

    // Generate edge nodes
    const edges: Edge[] = [];
    let edgeNodeIndex = 0;
    this.edgeNodes = [];

    if (!this.edges) this.edges = [];

    for (let i = 0; i < this.edges.length; i++) {
      const edge = this.edges[i][2];
      const illstNodes = Object.assign([], this.edges[i][0]);
      const mercNodes = Object.assign([], this.edges[i][1]);

      if (illstNodes.length === 0 && mercNodes.length === 0) {
        edges.push(edge);
        continue;
      }

      // Add start and end points
      illstNodes.unshift(this.points[edge[0]][0]);
      illstNodes.push(this.points[edge[1]][0]);
      mercNodes.unshift(this.points[edge[0]][1]);
      mercNodes.push(this.points[edge[1]][1]);

      // Calculate edge segments
      const segments = [illstNodes, mercNodes].map((nodes: Position[]): EdgeSegmentItem[] => {
        const lengths = nodes.map((node: Position, index: number, arr: Position[]) => {
          if (index === 0) return 0;
          const prev = arr[index - 1];
          return Math.sqrt(
            Math.pow(node[0] - prev[0], 2) + Math.pow(node[1] - prev[1], 2),
          );
        });

        const accumLengths = lengths.reduce((acc: number[], len: number, idx: number) => {
          if (idx === 0) return [0];
          acc.push(acc[idx - 1] + len);
          return acc;
        }, [] as number[]);

        return accumLengths.map((accum: number, idx: number, arr: number[]): EdgeSegmentItem => {
          const ratio = accum / arr[arr.length - 1];
          return [nodes[idx], lengths[idx], accumLengths[idx], ratio];
        });
      });

      // Generate edge nodes
      segments
        .map((segment: EdgeSegmentItem[], idx: number) => {
          const otherSegment = segments[idx ? 0 : 1];
          return segment
            .filter((item: EdgeSegmentItem, index: number) => {
              return !(
                index === 0 ||
                index === segment.length - 1 ||
                item[4] === "handled"
              );
            })
            .flatMap((item: EdgeSegmentItem): EdgeNodeItem[] => {
              const node = item[0];
              const ratio = item[3];

              const counterpart = otherSegment.reduce(
                (prev: EdgeSegmentItem[] | undefined, curr: EdgeSegmentItem, currIdx: number, arr: EdgeSegmentItem[]) => {
                  if (prev) return prev;
                  const nextItem = arr[currIdx + 1];
                  if (curr[3] === ratio) {
                    curr[4] = "handled";
                    return [curr];
                  }
                  if (
                    curr[3] < ratio && nextItem &&
                    nextItem[3] > ratio
                  ) {
                    return [curr, nextItem];
                  }
                  return undefined;
                },
                undefined as EdgeSegmentItem[] | undefined,
              );

              if (counterpart && counterpart.length === 1) {
                return idx === 0
                  ? [[node, counterpart[0][0], ratio]]
                  : [[counterpart[0][0], node, ratio]];
              }

              if (counterpart && counterpart.length === 2) {
                const curr = counterpart[0];
                const next = counterpart[1];
                const ratioInSegment = (ratio - (curr[3] as number)) /
                  ((next[3] as number) - (curr[3] as number));
                const interpNode: Position = [
                  (next[0][0] - curr[0][0]) *
                  ratioInSegment + curr[0][0],
                  (next[0][1] - curr[0][1]) *
                  ratioInSegment + curr[0][1],
                ];
                return idx === 0
                  ? [[node, interpNode, ratio]]
                  : [[interpNode, node, ratio]];
              }

              return [];
            });
        })
        .reduce((prev: EdgeNodeItem[], curr: EdgeNodeItem[]) => prev.concat(curr), [])
        .sort((a: EdgeNodeItem, b: EdgeNodeItem) => a[2] < b[2] ? -1 : 1)
        .map((item: EdgeNodeItem, index: number, arr: EdgeNodeItem[]) => {
          this.edgeNodes![edgeNodeIndex] = [
            item[0],
            item[1],
          ];
          const forPoint = createPoint(
            item[0],
            item[1],
            `e${edgeNodeIndex}`,
          );
          edgeNodeIndex++;
          pointsSet.forw!.push(forPoint);
          pointsSet.bakw!.push(counterPoint(forPoint));

          if (index === 0) {
            edges.push([edge[0], pointsSet.forw!.length - 1]);
          } else {
            edges.push([
              pointsSet.forw!.length - 2,
              pointsSet.forw!.length - 1,
            ]);
          }

          if (index === arr.length - 1) {
            edges.push([pointsSet.forw!.length - 1, edge[1]]);
          }
        });
    }

    return {
      forw: pointsSet.forw,
      bakw: pointsSet.bakw,
      edges,
    };
  }

  /**
   * 入力データの検証と初期データの準備
   */
  private validateAndPrepareInputs() {
    const minx = this.xy![0] - 0.05 * this.wh![0];
    const maxx = this.xy![0] + 1.05 * this.wh![0];
    const miny = this.xy![1] - 0.05 * this.wh![1];
    const maxy = this.xy![1] + 1.05 * this.wh![1];

    // Invariant: boundsPolygon is always set when bounds is set (see setBounds / setCompiled).
    if (this.bounds && !this.boundsPolygon) throw new Error("Internal error: bounds is set but boundsPolygon is missing");
    const bp = this.bounds ? this.boundsPolygon : undefined;
    const allPointsInside = this.points.reduce((prev: boolean, point: PointSet) => {
      return prev &&
        (bp
          ? booleanPointInPolygon(point[0] as Position, bp)
          : point[0][0] >= minx && point[0][0] <= maxx &&
          point[0][1] >= miny && point[0][1] <= maxy);
    }, true);

    if (!allPointsInside) {
      throw "SOME POINTS OUTSIDE";
    }

    let bbox: Position[] = [];
    if (this.wh) {
      bbox = [[minx, miny], [maxx, miny], [minx, maxy], [maxx, maxy]];
    }

    return {
      pointsSet: this.generatePointsSet(),
      bbox,
      minx,
      maxx,
      miny,
      maxy,
    };
  }

  /**
   * Compute a bounding box derived from GCP coordinates with a 5% margin.
   * Used in V3 plain mode where no explicit image bounds are available.
   */
  private computeGcpBbox(): {
    minx: number; maxx: number; miny: number; maxy: number;
  } {
    let gcpMinx = Infinity, gcpMaxx = -Infinity;
    let gcpMiny = Infinity, gcpMaxy = -Infinity;
    for (const p of this.points) {
      const x = p[0][0] as number, y = p[0][1] as number;
      if (x < gcpMinx) gcpMinx = x;
      if (x > gcpMaxx) gcpMaxx = x;
      if (y < gcpMiny) gcpMiny = y;
      if (y > gcpMaxy) gcpMaxy = y;
    }
    const gcpW = gcpMaxx - gcpMinx;
    const gcpH = gcpMaxy - gcpMiny;
    return {
      minx: gcpMinx - 0.05 * gcpW,
      maxx: gcpMaxx + 0.05 * gcpW,
      miny: gcpMiny - 0.05 * gcpH,
      maxy: gcpMaxy + 0.05 * gcpH,
    };
  }

  /**
   * TINネットワークを同期的に更新し、座標変換の準備を行います。
   * 重めの計算を伴うため、呼び出し側が非同期制御を行いたい場合は
   * {@link updateTinAsync} を利用してください。
   */
  updateTin(): void {
    let strict = this.strictMode;
    if (strict !== Tin.MODE_STRICT && strict !== Tin.MODE_LOOSE) {
      strict = Tin.MODE_AUTO;
    }

    const isV3 = !this.useV2Algorithm;
    let rawPointsSet: { forw: Feature<Point>[]; bakw: Feature<Point>[]; edges: Edge[] };
    let minx: number, maxx: number, miny: number, maxy: number;

    if (isV3) {
      // V3 (main map and submap): always derive bbox from GCPs.
      // When a bounds polygon (envelope) is provided, validate GCPs are inside it first.
      if (this.bounds) {
        // Invariant: boundsPolygon is always set when bounds is set (see setBounds / setCompiled).
        const bp = this.boundsPolygon;
        if (!bp) throw new Error("Internal error: bounds is set but boundsPolygon is missing");
        const allInsideBounds = this.points.every(
          (p: PointSet) => booleanPointInPolygon(p[0] as Position, bp),
        );
        if (!allInsideBounds) throw "SOME POINTS OUTSIDE";
      }
      rawPointsSet = this.generatePointsSet();
      ({ minx, maxx, miny, maxy } = this.computeGcpBbox());
    } else {
      // V2: use xy/wh bbox with full validation.
      const validated = this.validateAndPrepareInputs();
      rawPointsSet = validated.pointsSet;
      minx = validated.minx;
      maxx = validated.maxx;
      miny = validated.miny;
      maxy = validated.maxy;
    }

    // Create FeatureCollections for use in calculations
    const pointsSetFC = {
      forw: featureCollection(rawPointsSet.forw),
      bakw: featureCollection(rawPointsSet.bakw),
    };

    const tinForw = constrainedTin(
      pointsSetFC.forw,
      rawPointsSet.edges,
      "target",
    );
    const tinBakw = constrainedTin(
      pointsSetFC.bakw,
      rawPointsSet.edges,
      "target",
    );

    if (tinForw.features.length === 0 || tinBakw.features.length === 0) {
      throw "TOO LINEAR1";
    }

    const forCentroid = turfCentroid(pointsSetFC.forw);
    const forwConvex = convex(pointsSetFC.forw);
    if (!forwConvex) throw "TOO LINEAR2";

    const convexBuf: Record<string, { forw: Position; bakw: Position }> = {};
    const forwCoords = forwConvex.geometry!.coordinates[0];

    // Calculate forward convex hull transformation
    let convexCalc;
    try {
      convexCalc = forwCoords.map((coord: Position) => ({
        forw: coord,
        bakw: transformArr(point(coord), tinForw as Tins) as Position,
      }));
      convexCalc.forEach((item: { forw: Position; bakw: Position }) => {
        convexBuf[`${item.forw[0]}:${item.forw[1]}`] = item;
      });
    } catch {
      throw "TOO LINEAR2";
    }

    // Calculate backward convex hull transformation
    const bakwConvex = convex(pointsSetFC.bakw);
    if (!bakwConvex) throw "TOO LINEAR2";

    const bakwCoords = bakwConvex.geometry!.coordinates[0];
    try {
      convexCalc = bakwCoords.map((coord: Position) => ({
        bakw: coord,
        forw: transformArr(point(coord), tinBakw as Tins) as Position,
      }));
      convexCalc.forEach((item: { forw: Position; bakw: Position }) => {
        convexBuf[`${item.forw[0]}:${item.forw[1]}`] = item;
      });
    } catch {
      throw "TOO LINEAR2";
    }

    // Set centroids
    let centCalc: { forw: Position; bakw: Position };
    if (isV3) {
      // V3 (main map and submap): find the TIN triangle containing turf's centroid
      // and use its geometric centre (mean of 3 vertices) as the new centroid.
      const forCentCoord = forCentroid.geometry!.coordinates;
      const containingTri = (tinForw as Tins).features.find((tri: Tri) =>
        booleanPointInPolygon(
          point(forCentCoord),
          tri as unknown as Feature<Polygon>,
        )
      );
      if (containingTri) {
        const coords = containingTri.geometry!.coordinates[0];
        const aGeom = containingTri.properties!.a.geom as Position;
        const bGeom = containingTri.properties!.b.geom as Position;
        const cGeom = containingTri.properties!.c.geom as Position;
        centCalc = {
          forw: [
            (coords[0][0] + coords[1][0] + coords[2][0]) / 3,
            (coords[0][1] + coords[1][1] + coords[2][1]) / 3,
          ],
          bakw: [
            (aGeom[0] + bGeom[0] + cGeom[0]) / 3,
            (aGeom[1] + bGeom[1] + cGeom[1]) / 3,
          ],
        };
      } else {
        // Fallback: use turf centroid directly.
        centCalc = {
          forw: forCentCoord,
          bakw: transformArr(forCentroid, tinForw as Tins) as Position,
        };
      }
    } else {
      // V2: transform turf centroid through TIN.
      centCalc = {
        forw: forCentroid.geometry!.coordinates,
        bakw: transformArr(forCentroid, tinForw as Tins) as Position,
      };
    }

    const centroidPoint = createPoint(centCalc.forw, centCalc.bakw, "c");
    this.centroid = {
      forw: centroidPoint,
      bakw: counterPoint(centroidPoint),
    };

    // Calculate vertices
    // allGcps includes both GCPs and edge intermediate nodes so that
    // checkAndAdjustVerticesN can guarantee all constrained-edge bakw positions
    // are enclosed within the boundary polygon.
    const allGcps: BoundaryVerticesParams["allGcps"] = [
      ...this.points.map((p) => ({ forw: p[0] as Position, bakw: p[1] as Position })),
      ...(this.edgeNodes ?? []).map((n) => ({ forw: n[0] as Position, bakw: n[1] as Position })),
    ];
    const boundaryParams: BoundaryVerticesParams = {
      convexBuf,
      centroid: centCalc,
      allGcps,
      minx,
      maxx,
      miny,
      maxy,
    };

    // V3 enables the 36-bin edge vertex pass for both main maps and submaps.
    const verticesSet: VertexPosition[] = this.vertexMode === Tin.VERTEX_BIRDEYE
      ? calculateBirdeyeVertices(boundaryParams, isV3)
      : calculatePlainVertices(boundaryParams, isV3);

    // Add vertices to points set
    const verticesList = {
      forw: [] as Feature<Point>[],
      bakw: [] as Feature<Point>[],
    };
    for (let i = 0; i < verticesSet.length; i++) {
      const forw = verticesSet[i].forw;
      const bakw = verticesSet[i].bakw;
      const forPoint = createPoint(forw, bakw, `b${i}`);
      const bakPoint = counterPoint(forPoint);
      rawPointsSet.forw.push(forPoint);
      rawPointsSet.bakw.push(bakPoint);
      verticesList.forw.push(forPoint);
      verticesList.bakw.push(bakPoint);
    }

    this.pointsSet = {
      forw: featureCollection(rawPointsSet.forw),
      bakw: featureCollection(rawPointsSet.bakw),
      edges: rawPointsSet.edges,
    };

    // Generate forward TIN
    this.tins = {
      forw: rotateVerticesTriangle(
        constrainedTin(
          this.pointsSet!.forw,
          rawPointsSet.edges,
          "target",
        ) as Tins,
      ),
    };

    // Calculate strict TIN if needed
    if (strict === Tin.MODE_STRICT || strict === Tin.MODE_AUTO) {
      this.calcurateStrictTin();
    }

    // Generate backward TIN if needed
    if (
      strict === Tin.MODE_LOOSE ||
      (strict === Tin.MODE_AUTO && this.strict_status === Tin.STATUS_ERROR)
    ) {
      this.tins!.bakw = rotateVerticesTriangle(
        constrainedTin(
          this.pointsSet!.bakw,
          rawPointsSet.edges,
          "target",
        ) as Tins,
      );
      delete this.kinks;
      this.strict_status = Tin.STATUS_LOOSE;
    }

    // Calculate vertices parameters
    this.vertices_params = {
      forw: vertexCalc(verticesList.forw, this.centroid.forw!),
      bakw: vertexCalc(verticesList.bakw, this.centroid.bakw!),
    };

    this.addIndexedTin();

    const targets: Array<keyof TinsBD> = ["forw"];
    if (this.strict_status === Tin.STATUS_LOOSE) {
      targets.push("bakw");
    }

    const includeReciprocals = this.strict_status === Tin.STATUS_STRICT;
    this.pointsWeightBuffer = buildPointsWeightBuffer({
      tins: this.tins!,
      targets,
      includeReciprocals,
      numBoundaryVertices: verticesSet.length,
    });
  }

  /**
   * 非同期ラッパーを提供します。
   * 互換性のために Promise ベースの API を維持しますが、内部処理は同期的です。
   */
  async updateTinAsync(): Promise<void> {
    this.updateTin();
  }
}
