import NativeSMapOffline from './NativeSMapOffline';

/** An area of the map to keep on the device: one style in one language. */
export interface OfflineRegion {
  /** The app's own name for the region, unique on the device (1–64 letters, digits, `-`, `_`, `.`). */
  id: string;
  west: number;
  south: number;
  east: number;
  north: number;
  minZoom?: number;
  /** The map's own maximum when left out. */
  maxZoom?: number;
  /** A scheme (`light`, …) or `custom:<id>`; `light` by default. */
  style?: string;
  /** Label language; the device language by default. */
  language?: string;
}

/** What a region would be (asked before downloading; counts nothing). */
export interface OfflineEstimate {
  tiles: number;
  bytes: number;
  maxBytes: number;
  /** False when the region is larger than one region file may be: split it or lower its zoom. */
  fits: boolean;
  /** False when this app's key may not download offline regions (a token always may). */
  allowed: boolean;
}

/** A region kept on the device. `expired`: past its 30 days, its map data removed until it is refreshed. */
export interface OfflineInfo {
  region: OfflineRegion;
  state: 'ready' | 'expired';
  language: string;
  tiles: number;
  bytes: number;
  /** Milliseconds since 1970, the map service's clock. */
  downloadedAt: number;
  expiresAt: number;
}

/** Where offline regions are read from: the same access as `<SMap>`. */
export interface OfflineOptions {
  apiKey?: string;
  token?: string;
  getToken?: () => string | Promise<string>;
  baseUrl?: string;
  nodes?: string[];
  nearest?: boolean;
  language?: string;
}

/** Called while a region downloads: bytes received so far and the file's size. */
export type OfflineProgress = (done: number, total: number) => void;

function native() {
  if (!NativeSMapOffline) { throw new Error('sMap: offline regions are not linked (Android and iOS only)'); }
  return NativeSMapOffline;
}

/**
 * Offline regions: areas of the map downloaded as one file each and shown without a network
 * (`<SMap offlineRegion="…">`, or `map.useOfflineRegion(id)`). A download continues where it stopped when it is
 * called again for the same region. Only a token, or a map key whose *Allow offline regions* switch is on, may
 * download; each download is billed as the region's tiles. A region is shown for 30 days from its download.
 */
export function offlineRegions(options: OfflineOptions = {}) {
  const config = async () => JSON.stringify({
    baseUrl: options.baseUrl || null,
    nodes: options.nodes || [],
    apiKey: options.apiKey || null,
    token: (options.getToken ? await options.getToken() : options.token) || null,
    nearest: options.nearest !== false,
    language: options.language || null,
  });
  const parse = <T>(text: string): T => JSON.parse(text) as T;
  // Progress events of one region while its download runs; every listener is removed when the download ends.
  const withProgress = async <T>(id: string, onProgress: OfflineProgress | undefined, run: () => Promise<T>): Promise<T> => {
    const sub = onProgress ? native().onProgress((e) => { if (e.id === id) { onProgress(e.done, e.total); } }) : null;
    try {
      return await run();
    } finally {
      sub?.remove();
    }
  };
  return {
    estimate: async (region: OfflineRegion) => parse<OfflineEstimate>(await native().estimate(await config(), JSON.stringify(region))),
    /** Downloads the region; `onProgress` gets the bytes received and the file's size while it runs. */
    download: async (region: OfflineRegion, onProgress?: OfflineProgress) => {
      const c = await config();
      return withProgress(region.id, onProgress, async () => parse<OfflineInfo>(await native().download(c, JSON.stringify(region))));
    },
    refresh: async (id: string, onProgress?: OfflineProgress) => {
      const c = await config();
      return withProgress(id, onProgress, async () => parse<OfflineInfo>(await native().refresh(c, id)));
    },
    list: async () => parse<OfflineInfo[]>(await native().list(await config())),
    get: async (id: string) => parse<OfflineInfo | null>(await native().get(await config(), id)),
    remove: async (id: string) => { await native().remove(await config(), id); },
    usage: async () => parse<number>(await native().usage(await config())),
  };
}
