/*
 * kuon_bases.h — RTK基準局を、装置に選ばせる
 *
 * 現在地を渡すと、公開されているRTK基準局の一覧を取りに行き、
 * その場で距離を測って、近い順に返します。接続そのものは行いません。
 * いま使っている NTRIP クライアントの設定に host / port / mount を入れるだけです。
 *
 *   一覧の出どころ : https://kuongeo.com/api/bases  （認証なし・鍵なし・無料）
 *   解説           : https://kuongeo.com/ja/learn/bases
 *
 * 設計の約束
 *   ・一覧を全部メモリに載せません。1行ずつ流し読みして、近い順の上位だけを残します。
 *     417局あってもマイコンのRAMを食いません（保持するのは max 件ぶんだけ）。
 *   ・有料の局や認証の要る局を、この部品は弾きません。印をそのまま渡します。
 *     勝手に課金される道を塞ぐのは、装置を作る側の責任だと考えているためです。
 *   ・Fix は約束できません。基線長・アンテナ・空の見え方で決まります。
 *     carrSoln（0=単独 1=Float 2=Fixed）は、そのまま画面に出してください。
 *     Float を cm と偽らないことが、この分野の最低限の作法です。
 *
 * 対象   ESP32 / ESP8266（Arduino core）。標準ライブラリだけで動きます。
 * 使い方 このファイルを .ino と同じ場所に置いて #include "kuon_bases.h"
 *
 * ------------------------------------------------------------------
 * MIT License
 * Copyright (c) 2026 朝比奈幸太郎 / KUON GEO (kuongeo.com)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 * ------------------------------------------------------------------
 */
#ifndef KUON_BASES_H
#define KUON_BASES_H

#include <Arduino.h>
#if defined(ESP32)
  #include <WiFiClientSecure.h>
#elif defined(ESP8266)
  #include <WiFiClientSecure.h>
#else
  #error "kuon_bases.h は ESP32 / ESP8266 向けです"
#endif

#ifndef KUON_HOST_LEN
#define KUON_HOST_LEN 40
#endif
#ifndef KUON_MOUNT_LEN
#define KUON_MOUNT_LEN 32
#endif

typedef struct {
  char  host[KUON_HOST_LEN];   // キャスターのアドレス
  uint16_t port;               // ポート（現状はすべて 2101）
  char  mount[KUON_MOUNT_LEN]; // マウントポイント
  float lat, lon;              // 局の位置
  float km;                    // ここからの距離（大円距離）
  bool  needsAuth;             // 認証が要るか（TSVの auth が 'N' 以外）
  bool  paid;                  // 有料か（TSVの fee が 'N' 以外）
  bool  online;                // 直近の巡回で応答したか
  uint16_t polls30;            // 直近30日に応答した回数（最大 144×30）
} kuon_base_t;

/* 大円距離（km）。地球半径6371km。マイコンでも十分な精度。 */
static float kuon_haversine(float la1, float lo1, float la2, float lo2) {
  const float R = 6371.0f, r = 0.017453292519943295f;
  float d1 = (la2 - la1) * r, d2 = (lo2 - lo1) * r;
  float a = sinf(d1 / 2) * sinf(d1 / 2)
          + cosf(la1 * r) * cosf(la2 * r) * sinf(d2 / 2) * sinf(d2 / 2);
  if (a < 0) a = 0; if (a > 1) a = 1;
  return 2.0f * R * asinf(sqrtf(a));
}

/* TSVの1行から1局を取り出す。列は
   host port mount lat lon c f det nav gen bps auth fee p30 up ago */
static bool kuon_parse_line(const String &ln, float lat, float lon, kuon_base_t *out) {
  if (ln.length() < 8 || ln[0] == '#' || ln.startsWith("host\t")) return false;
  int col = 0, from = 0;
  String v[16];
  while (col < 16) {
    int t = ln.indexOf('\t', from);
    v[col++] = (t < 0) ? ln.substring(from) : ln.substring(from, t);
    if (t < 0) break;
    from = t + 1;
  }
  if (col < 15) return false;                 // 列が足りない行は捨てる
  float bla = v[3].toFloat(), blo = v[4].toFloat();
  if (bla == 0.0f && blo == 0.0f) return false; // 位置不明の局は選べない
  memset(out, 0, sizeof(*out));
  strncpy(out->host,  v[0].c_str(), KUON_HOST_LEN - 1);
  out->port = (uint16_t)v[1].toInt(); if (!out->port) out->port = 2101;
  strncpy(out->mount, v[2].c_str(), KUON_MOUNT_LEN - 1);
  out->lat = bla; out->lon = blo;
  out->km  = kuon_haversine(lat, lon, bla, blo);
  out->needsAuth = !(v[11] == "N" || v[11].length() == 0);
  out->paid      = !(v[12] == "N" || v[12].length() == 0);
  out->polls30   = (uint16_t)v[13].toInt();
  out->online    = (v[14].toInt() == 1);
  return true;
}

/*
 * 近い順に基準局を探す。
 *   lat, lon   いまの位置（度）
 *   maxKm      これより遠い局は捨てる。0 なら距離で捨てない
 *   out, max   結果の置き場と件数。近い順に詰めて返す
 *   jpOnly     日本域だけに絞るなら true（一覧が小さくなり通信が速い）
 *   onlineOnly 直近の巡回で応答した局だけにするなら true
 * 戻り値       見つかった件数。通信に失敗したら -1
 *
 * 一覧は500件ずつ取りに行き、必要なだけ繰り返します。
 * 保持するのは max 件だけなので、局が何局あってもRAMは増えません。
 */
static int kuon_find_bases(float lat, float lon, float maxKm,
                           kuon_base_t *out, int max,
                           bool jpOnly = true, bool onlineOnly = true) {
  if (!out || max <= 0) return 0;
  const char *HOST = "kuongeo.com";
  int found = 0, offset = 0, total = -1;
  const int PAGE = 500;

  while (true) {
    WiFiClientSecure cli;
    cli.setInsecure();          // 公開データの取得のみ。鍵も個人情報も送りません。
    cli.setTimeout(15000);
    if (!cli.connect(HOST, 443)) return found ? found : -1;

    String path = String("/api/bases?fmt=tsv&lim=") + PAGE + "&off=" + offset;
    if (jpOnly) path += "&jp=1";
    cli.print(String("GET ") + path + " HTTP/1.1\r\n" +
              "Host: " + HOST + "\r\n" +
              "User-Agent: kuon_bases.h/1.0\r\n" +
              "Accept: text/plain\r\n" +
              "Connection: close\r\n\r\n");

    // ヘッダを読み飛ばす
    while (cli.connected()) {
      String h = cli.readStringUntil('\n');
      if (h == "\r" || h.length() == 0) break;
    }

    int gotThisPage = 0;
    while (cli.connected() || cli.available()) {
      String ln = cli.readStringUntil('\n');
      if (!ln.length()) { if (!cli.available() && !cli.connected()) break; continue; }
      ln.trim();
      if (ln.startsWith("# total=")) {              // 総数を控えて周回の終わりを決める
        int a = ln.indexOf('=') + 1, b = ln.indexOf(' ', a);
        total = ln.substring(a, b).toInt();
        continue;
      }
      kuon_base_t b;
      if (!kuon_parse_line(ln, lat, lon, &b)) continue;
      gotThisPage++;
      if (onlineOnly && !b.online) continue;
      if (maxKm > 0 && b.km > maxKm) continue;

      // 近い順の挿入。max 件を超えたら遠いものから落ちる。
      int pos = found;
      while (pos > 0 && out[pos - 1].km > b.km) pos--;
      if (pos >= max) continue;
      int last = (found < max) ? found : max - 1;
      for (int i = last; i > pos; i--) out[i] = out[i - 1];
      out[pos] = b;
      if (found < max) found++;
    }
    cli.stop();

    offset += PAGE;
    if (total < 0 || offset >= total || gotThisPage == 0) break;
  }
  return found;
}

/*
 * 使い方の例（そのまま .ino に貼れます）
 *
 *   #include <WiFi.h>
 *   #include "kuon_bases.h"
 *
 *   void setup() {
 *     Serial.begin(115200);
 *     WiFi.begin("SSID", "PASSWORD");
 *     while (WiFi.status() != WL_CONNECTED) delay(300);
 *
 *     kuon_base_t near[10];
 *     int n = kuon_find_bases(42.9236, 143.1961, 50.0, near, 10);   // 帯広から50km以内
 *     for (int i = 0; i < n; i++) {
 *       Serial.printf("%6.1f km  %s:%u/%s%s%s\n",
 *         near[i].km, near[i].host, near[i].port, near[i].mount,
 *         near[i].needsAuth ? "  [認証あり]" : "",
 *         near[i].paid      ? "  [有料]"     : "");
 *     }
 *
 *     // いちばん近い局を、いま使っている NTRIP の設定に入れる
 *     if (n > 0 && !near[0].needsAuth && !near[0].paid) {
 *       // ntrip.begin(near[0].host, near[0].port, near[0].mount);
 *     }
 *   }
 *   void loop() {}
 *
 * 注意
 *   ・needsAuth と paid の扱いは、使う側で必ず決めてください。
 *     この部品は印を渡すだけで、判断はしません。
 *   ・maxKm は用途で決まります。10km以内なら数十秒でFixに入る見込みですが、
 *     40kmを超えると入らないことが増えます。
 *   ・一覧は配信側で120秒キャッシュしています。何台から呼んでも負荷は増えません。
 *     起動時に1回と、大きく移動したときだけ呼べば十分です。
 */
#endif /* KUON_BASES_H */
