import { readFileSync, writeFileSync } from "node:fs";
import sharp from "sharp";

/**
 * 経度からX方向の座標情報を計算します
 *
 * 指定された経度とズームレベルに基づき、以下の4つの値を返します：
 * - 世界座標系でのX（0〜1の範囲、グリニッジ子午線が0）
 * - ズームレベルを考慮したピクセル単位のグローバルX座標
 * - タイルグリッドにおけるX方向のタイル番号（tileNumberX）
 * - タイル内（256×256ピクセル）でのX方向ピクセル位置（tileInnerX）
 * @param lat - 経度
 * @param z - ズームレベル（0以上の整数）
 * @returns {{
 *   worldCoordX: number, // 世界座標(0～1, グリニッジ子午線が原点(0))
 *   globalPixelX: number, // ズームレベルを考慮したピクセル単位のY座標
 *   tileNumberX: number, // 該当するタイル番号（0オリジン）
 *   tileInnerX: number, // タイル内のピクセルY座標（0〜255）
 * }}
 * @copyright murasuke on Qiita - https://qiita.com/murasuke/items/ad81b7b726a3463fa3fe#%E7%B5%8C%E5%BA%A6
 */
function calcLngToTileX(lng: number, z: number) {
  // 経度(-180〜180)を世界座標系のX(0〜1)に変換
  const worldCoordX = (lng + 180) / 360;

  // ズームレベル0(256×256px)の座標の位置を計算
  let globalPixelX = worldCoordX * 256;

  // ズームレベル(2^z)を考慮した座標を計算
  globalPixelX = globalPixelX * Math.pow(2, z);

  // 1つの画像タイルが256pxなので、256で割って左端からのタイルの枚数(位置)を求める
  const tileNumberX = Math.floor(globalPixelX / 256);

  // 該当タイル内のピクセル位置を算出する(タイル幅合計を引いた余り)
  const tileInnerX = Math.floor(globalPixelX - tileNumberX * 256);

  // 計算した値を返す
  return {
    worldCoordX, // 世界座標(0～1, グリニッジ子午線が原点(0))
    globalPixelX, // ズームレベルを考慮したピクセル単位のY座標
    tileNumberX, // 該当するタイル番号（0オリジン）
    tileInnerX, // タイル内のピクセルY座標（0〜255）
  };
}

/**
 * 緯度からWebメルカトル投影によるY方向の座標情報を計算します。
 *
 * 指定された緯度とズームレベルに基づき、以下の4つの値を返します：
 * - 世界座標系でのY（0〜1の範囲、北極が0）
 * - ズームレベルを考慮したピクセル単位のグローバルY座標
 * - タイルグリッドにおけるY方向のタイル番号（tileNumberY）
 * - タイル内（256×256ピクセル）でのY方向ピクセル位置（tileInnerY）
 *
 * メルカトル図法で緯度から位置を算出する式 (https://qiita.com/Seo-4d696b75/items/aa6adfbfba404fcd65aa)
 *  R ln(tan(π/4 + ϕ/2))
 *    R: 半径
 *    ϕ: 緯度(ラジアン)
 * @param lat - 緯度（-85〜+85度の範囲が推奨）
 * @param z - ズームレベル（0以上の整数）
 * @returns {{
 *   worldCoordY: number,    // 世界座標系Y（0〜1）
 *   globalPixelY: number,   // ズームレベルを考慮したピクセル単位のY座標
 *   tileNumberY: number,    // Y方向のタイル番号（0オリジン）
 *   tileInnerY: number      // タイル内のピクセルY座標（0〜255）
 * }}
 * @copyright murasuke on Qiita - https://qiita.com/murasuke/items/ad81b7b726a3463fa3fe#%E7%B7%AF%E5%BA%A6
 */
function calcLatToTileY(lat: number, z: number) {
  // 緯度からメルカトル図法の座標に変換して、世界座標系のY(0～1)を計算
  let worldCoordY = latToWorldY(lat);

  // ズームレベル0(256×256px)の座標の位置を計算
  let globalPixelY = worldCoordY * 256;

  // ズームレベル(2^z)を考慮した座標を計算
  globalPixelY = globalPixelY * Math.pow(2, z);

  // 1つの画像タイルが256pxなので、256で割って上端からのタイルの枚数(位置)を求める
  const tileNumberY = Math.floor(globalPixelY / 256);

  // 該当タイル内のピクセル位置を算出する(タイル幅合計を引いた余り)
  const tileInnerY = Math.floor(globalPixelY - tileNumberY * 256);

  // 計算した値を返す
  return {
    worldCoordY, // 世界座標(0～1, 北極側が原点(0))
    globalPixelY, // ズームレベルを考慮したピクセル単位のY座標
    tileNumberY, // 該当するタイル番号（0オリジン）
    tileInnerY, // タイル内のピクセルY座標（0〜255）
  };
}

/**
 * メルカトル図法で投影した位置を元に世界座標系でのY(0〜1、北極が0)を計算する
 *
 * メルカトル図法の式（半径1の球体を想定）
 *   ϕ = (π / 180) * 緯度
 *   y = ln(tan(π/4 + ϕ/2))
 *   ϕ:緯度(ラジアン)
 *   y:投影した位置
 *
 * * 赤道を中心(0)にして、約-85〜+85度の範囲で-π～πの値になる
 * * 高緯度では値が急増し無限大になる
 * * y = ln((tan(ϕ) + 1) / cos(ϕ)) という式もある(同じ値になる)
 *
 * 地図で位置を計算するため、北極側が原点(0)で南極側が1となるように正規化した値を返す
 *
 * @param lat 緯度(-85～85)
 * @returns {number} 世界座標系でのY（0〜1、北極が0）
 * @copyright murasuke on Qiita - https://qiita.com/murasuke/items/ad81b7b726a3463fa3fe#%E7%B7%AF%E5%BA%A6
 */
function latToWorldY(lat: number) {
  // 経度をラジアンに変換
  const latRad = (Math.PI / 180) * lat;
  // 緯度からメルカトル図法の座標に変換する
  const mercatorY = Math.log(Math.tan(Math.PI / 4 + latRad / 2));

  // -1～1 の範囲になるように調整(-85度～85度の範囲)
  let worldCoordY = mercatorY / Math.PI;

  // 原点(0)を赤道から、北極へ変換(0～1)
  worldCoordY = 1 - (worldCoordY + 1) / 2;

  return worldCoordY;
}

function getEdge<T extends any[]>(arr: T, property: keyof T[number]) {
  if (arr.length === 0) {
    return {
      most: undefined,
      least: undefined,
    }
  }

  const counts = new Map<any, number>();
  for (const item of arr) {
    const val = item[property];
    counts.set(val, (counts.get(val) || 0) + 1);
  }

  const uniqueValues = Array.from(counts.keys());
  
  const minVal = uniqueValues.reduce((a, b) => (a < b ? a : b));
  const maxVal = uniqueValues.reduce((a, b) => (a > b ? a : b));

  return {
    most: maxVal,
    least: minVal,
  }
}

export default async function generateImage(message: any) {
  if (
    message.earthquake.hypocenter === undefined ||
    message.earthquake.hypocenter?.name === "" ||
    !(Array.isArray(message.points)) ||
    message.points.length === 0
  )
    return "input_lack";

  const ZOOM_LEVEL = 9;
  
  // タイル・地点取得
  const tiles: {
    tileX: number;
    tileY: number;
  }[] = [];

  const positions: ({
    tileIndex: number;
    innerX: number;
    innerY: number;
  } & ({
    type: "epicenter";
  } | {
    type: "point";
    scale: number;
  }))[] = [];

  if (
    message.earthquake.hypocenter?.latitude !== undefined &&
    message.earthquake.hypocenter?.latitude !== -200 &&
    message.earthquake.hypocenter?.longitude !== undefined &&
    message.earthquake.hypocenter?.longitude !== -200
  ) {
    // 震源地
    const lng = message.earthquake.hypocenter.longitude;
    const lat = message.earthquake.hypocenter.latitude;
    const xData = calcLngToTileX(lng, ZOOM_LEVEL);
    const yData = calcLatToTileY(lat, ZOOM_LEVEL);

    tiles.push({
      tileX: xData.tileNumberX,
      tileY: yData.tileNumberY,
    });

    positions.push({
      tileIndex: 0,
      innerX: xData.tileInnerX,
      innerY: yData.tileInnerY,
      type: "epicenter",
    });
  }

  if (
    Array.isArray(message.points) &&
    message.points.length > 0
  ) {
    // 観測点・区域
    const areas = JSON.parse(readFileSync(`${import.meta.dirname}/areas.json`, "utf-8"));
    const points = JSON.parse(readFileSync(`${import.meta.dirname}/points.json`, "utf-8"));
    message.points.forEach((point: any) => {
      let areaData: {
        longitude: number;
        latitude: number;
      }

      if (point.isArea) {
        areaData = areas[point.addr];
      } else {
        areaData = points[point.addr];
      }

      const lng = areaData.longitude;
      const lat = areaData.latitude;
      const xData = calcLngToTileX(lng, ZOOM_LEVEL);
      const yData = calcLatToTileY(lat, ZOOM_LEVEL);
      
      const tilePosition = {
        tileX: xData.tileNumberX,
        tileY: yData.tileNumberY,
      }

      let tileIndex: number;

      const sameTile = tiles
        .map((value, index) => ({ value, index }))
        .filter(tile => JSON.stringify(tile.value) === JSON.stringify(tilePosition))[0];

      if (sameTile) {
        tileIndex = sameTile.index;
      } else {
        tiles.push(tilePosition);
        tileIndex = tiles.length - 1;
      }

      if (tiles.filter(tile => JSON.stringify(tile) === JSON.stringify(tilePosition)).length === 0) {
        tiles.push(tilePosition);
      }

      positions.push({
        tileIndex,
        innerX: xData.tileInnerX,
        innerY: yData.tileInnerY,
        type: "point",
        scale: point.scale,
      });
    });
  }

  // 画像生成
  const WIDTH = 1920;
  const HEIGHT = 1080;

  let result = sharp({
    create: {
      width: WIDTH,
      height: HEIGHT,
      channels: 4,
      background: "#ffffff",
    }
  });

  let compounds: (sharp.OverlayOptions & ({
    type: "tile" | "epicenter" | "other";
  } | {
    type: "scale";
    scale: number;
  }))[] = [];

  // タイルの幅、最大最小
  let tileXCount = getEdge(tiles, "tileX");
  let tileYCount = getEdge(tiles, "tileY");
  let xSize = tileXCount.most - tileXCount.least + 1;
  let ySize = tileYCount.most - tileYCount.least + 1;

  // タイルサイズ仮計算
  let tileSize: number;

  xSize = tileXCount.most - tileXCount.least + 1;
  ySize = tileYCount.most - tileYCount.least + 1;
  if (xSize > ySize) {
    tileSize = Math.round(WIDTH / xSize);
  } else {
    tileSize = Math.round(HEIGHT / ySize);
  }

  // 震源を中心とする
  const epicenterPosition = positions.filter(position => position.type === "epicenter")[0];
  if (!epicenterPosition)
    return "input_lack";

  const epicenterTile = tiles[epicenterPosition.tileIndex];
  if (!epicenterTile)
    return "input_lack";

  const leftPx = (epicenterTile.tileX - tileXCount.least) * tileSize + epicenterPosition.innerX;
  const rightPx = xSize * tileSize - leftPx;
  const topPx = (epicenterTile.tileY - tileYCount.least) * tileSize + epicenterPosition.innerY;
  const bottomPx = ySize * tileSize - topPx;

  if (leftPx > rightPx) {
    const leftTiles = epicenterTile.tileX - tileXCount.least;
    tileXCount = {
      ...tileXCount,
      most: epicenterTile.tileX + leftTiles,
    }
  } else {
    const rightTiles = tileXCount.most - epicenterTile.tileX;
    tileXCount = {
      ...tileXCount,
      least: epicenterTile.tileX - rightTiles,
    }
  }

  if (topPx > bottomPx) {
    const topTiles = epicenterTile.tileY - tileYCount.least;
    tileYCount = {
      ...tileYCount,
      most: epicenterTile.tileY + topTiles,
    }
  } else {
    const bottomTiles = tileYCount.most - epicenterTile.tileY;
    tileYCount = {
      ...tileYCount,
      least: epicenterTile.tileY - bottomTiles,
    }
  }

  // タイルサイズ再計算
  xSize = tileXCount.most - tileXCount.least + 1;
  ySize = tileYCount.most - tileYCount.least + 1;
  if (xSize > ySize) {
    tileSize = Math.round(WIDTH / xSize);
  } else {
    tileSize = Math.round(HEIGHT / ySize);
  }

  // 全画面
  if (WIDTH - xSize * tileSize > 5) {
    const requireTilesX = Math.ceil(WIDTH / tileSize);
    const halfTilesX = Math.ceil(requireTilesX / 2);

    tileXCount = {
      least: tileXCount.least - halfTilesX,
      most: tileXCount.most + halfTilesX,
    }
  }

  if (HEIGHT - ySize * tileSize > 5) {
    const requireTilesY = Math.ceil(HEIGHT / tileSize);
    const halfTilesY = Math.ceil(requireTilesY / 2);

    tileYCount = {
      least: tileYCount.least - halfTilesY,
      most: tileYCount.most + halfTilesY,
    }
  }

  // タイル幅再計算
  xSize = tileXCount.most - tileXCount.least + 1;
  ySize = tileYCount.most - tileYCount.least + 1;

  // 欠けているタイルを取得
  for (let xIndex = 0; xIndex < xSize; xIndex++) {
    const itX = xIndex + tileXCount.least;
    
    for (let yIndex = 0; yIndex < ySize; yIndex++) {
      const itY = yIndex + tileYCount.least;

      const itTile = tiles.filter(tile => tile.tileX === itX && tile.tileY === itY)[0];
      if (itTile)
        continue;

      tiles.push({
        tileX: itX,
        tileY: itY,
      });
    }
  }

  // アセット
  const assets: Record<string, {
    buffer: Buffer<ArrayBuffer>;
    size: number;
  }> = {}
  const assetsList = [
    "scales/10",
    "scales/20",
    "scales/30",
    "scales/40",
    "scales/45",
    "scales/46",
    "scales/50",
    "scales/55",
    "scales/60",
    "scales/70",
    "epicenter",
  ];

  await Promise.all(assetsList.map(async name => {
    const key = name.split("/").at(-1);
    if (!key)
      return;

    const asset = readFileSync(`${import.meta.dirname}/assets/${name}.png`);
    const assetMeta = await sharp(asset).metadata();
    const assetSize = assetMeta.width;

    assets[key] = {
      buffer: asset,
      size: assetSize,
    }
  }));

  // 各画像処理
  await Promise.all(tiles.map(async (tile, index) => {
    const TYPE = "blank";
    const BASE_URL = "https://cyberjapandata.gsi.go.jp/xyz";

    const request = await fetch(`${BASE_URL}/${TYPE}/${ZOOM_LEVEL}/${tile.tileX}/${tile.tileY}.png`);

    if (!request.ok)
      return;

    const tileImg = await sharp(await request.arrayBuffer()).resize({
      width: tileSize,
      height: tileSize,
    }).png().toBuffer();

    const top = tileSize * (tile.tileY - tileYCount.least);
    const left = tileSize * (tile.tileX - tileXCount.least);

    compounds.push({
      type: "tile",
      input: tileImg,
      top,
      left,
    });

    const itTilePositions = positions.filter(position => position.tileIndex === index);
    itTilePositions.forEach(position => {
      const requireAssetName = position.type === "epicenter"
        ? "epicenter"
        : String(position.scale);

      const asset = assets[requireAssetName];
      if (!asset)
        return;

      const tileResizedSize = tileSize / 256;

      let about: any;

      if (position.type === "epicenter") {
        about = {
          type: "epicenter",
        }
      } else {
        about = {
          type: "scale",
          scale: position.scale,
        }
      }

      compounds.push({
        ...about,
        input: asset.buffer,
        top: Math.round(top + position.innerY * tileResizedSize - asset.size / 2),
        left: Math.round(left + position.innerX * tileResizedSize - asset.size / 2),
      });
    });
  }));

  // compounds並び替え
  const TYPE_PRIORITY: Record<string, number> = {
    tile: 1,
    scale: 2,
    epicenter: 3,
  }

  compounds = [...compounds].sort((a, b) => {
    if (TYPE_PRIORITY[a.type] !== TYPE_PRIORITY[b.type]) {
      return (TYPE_PRIORITY[a.type] ?? 99) - (TYPE_PRIORITY[b.type] ?? 99);
    }

    if (a.type === "scale" && b.type === "scale") {
      return a.scale - b.scale;
    }

    return 0;
  });

  // 案内部分
  const informationPath = `${import.meta.dirname}/assets/information.png`;
  const informationBuffer = readFileSync(informationPath);
  const informationMeta = await sharp(informationPath).metadata();
  const informationMargin = 20;
  compounds.push({
    type: "other",
    input: informationBuffer,
    top: HEIGHT - informationMeta.height - informationMargin,
    left: WIDTH - informationMeta.width - informationMargin,
  });

  // クレジット部分
  const creditPath = `${import.meta.dirname}/assets/credit.png`;
  const creditBuffer = readFileSync(creditPath);
  const creditMeta = await sharp(creditPath).metadata();
  compounds.push({
    type: "other",
    input: creditBuffer,
    top: HEIGHT - creditMeta.height,
    left: 0,
  });

  result.composite(compounds);

  const buffer = await result.png().toBuffer();
  return buffer;
}