import OpenLocationCode from './open_location_code.ts'
import parseGeoUri from './parse_geouri.ts'
import { nominatimSearch } from './nominatim.ts'

export type DirectCoords = { lat: number; lng: number; zoom: number }

export async function coordsFromDirectInput(
  query: string,
): Promise<DirectCoords | null> {
  return coordsFromGeoUri(query) ?? coordsFromPlusCode(query) ??
    coordsFromMapUrl(query) ?? await coordsFromLongPlusCode(query)
}

/**
 * Coordinate extraction from pasted map URLs (Google, OSM, Apple).
 *
 * @reference https://developers.google.com/maps/documentation/urls/get-started
 * @example https://www.google.com/maps/place/Glorieta+de+los+Insurgentes/@19.4181529,-99.1652013,16z/...
 *
 * Known issues:
 *   - Short links (maps.app.goo.gl) carry no coordinates in the URL itself,
 *     so they can't be resolved without fetching them.
 */

const matchAppleCoords =
  /(?:\?|&)ll=(-?\d{1,2}(?:\.\d+)?),(-?\d{1,3}(?:\.\d+)?)/
const matchAppleAddress = /(?:\?|&)address=([^&]+)/

function getCoordsFromAppleMapsUrl(url: string) {
  const [_coords, lat, lon] = url.match(matchAppleCoords) || [null, null, null]
  const [_address, address] = url.match(matchAppleAddress) || [null, null]
  if (lon && lat) {
    return {
      lon: parseFloat(lon),
      lat: parseFloat(lat),
      display_name: address?.length ? decodeAddress(address) : '',
    }
  }
}

// Regex for matching lat lon from a url
const matchGooglemapsCoords =
  /@([-+]?\d{1,2}(?:\.\d+)?),([-+]?\d{1,3}(?:\.\d+)?)/
const matchGoogleAddress = /(?:\?|&)address=([^&]+)/

function getCoordsFromGoogleMapsLongUrl(url: string) {
  const [_, lat, lon] = url.match(matchGooglemapsCoords) || [null, null, null]
  const [_address, address] = url.match(matchGoogleAddress) || [null, null]

  if (lon && lat) {
    return {
      lon: parseFloat(lon),
      lat: parseFloat(lat),
      display_name: address?.length ? decodeAddress(address) : '',
    }
  }
}

function getCoordsFromOSMUrl(url: string) {
  const matchLat = /mlat=([-+]?\d{1,2}(?:\.\d+)?)/
  const matchLon = /mlon=([-+]?\d{1,3}(?:\.\d+)?)/

  const latMatch = url.match(matchLat)
  const lonMatch = url.match(matchLon)

  if (latMatch && lonMatch) {
    const lat = parseFloat(latMatch[1])
    const lon = parseFloat(lonMatch[1])
    return { lat, lon, display_name: '' }
  } else {
    return null
  }
}

function decodeAddress(encodedAddress = '') {
  const decodedAddress = decodeURIComponent(encodedAddress)
  const readableAddress = decodedAddress.replace(/\+/g, ' ')
  return readableAddress
}

function coordsFromGeoUri(query: string): DirectCoords | null {
  const coords = parseGeoUri(query)
  if (!coords) return null
  let zoom = 15
  if (coords.accuracy != null) {
    if (coords.accuracy > 50000) zoom = 7
    else if (coords.accuracy > 10000) zoom = 9
    else if (coords.accuracy > 1000) zoom = 13
  }
  return { lat: coords.latitude, lng: coords.longitude, zoom }
}

function coordsFromMapUrl(query: string): DirectCoords | null {
  let result
  try {
    result = getCoordsFromGoogleMapsLongUrl(query)
  } catch { /* */ }
  if (!result?.lat || !result?.lon) {
    try {
      result = getCoordsFromOSMUrl(query)
    } catch { /* */ }
  }
  if (!result?.lat || !result?.lon) {
    try {
      result = getCoordsFromAppleMapsUrl(query)
    } catch { /* */ }
  }
  if (!result?.lat || !result?.lon) return null
  return { lat: result.lat, lng: result.lon, zoom: 15 }
}

// Matches short plus codes with a location suffix, e.g. "2HM7+64 Xinyi District, Taipei City, Taiwan"
const shortPlusCodePattern =
  /^([23456789CFGHJMPQRVWXcfghjmpqrvwx]{2,7}\+[23456789CFGHJMPQRVWXcfghjmpqrvwx]{2,3})\s+(.+)$/

function coordsFromPlusCode(query: string): DirectCoords | null {
  const trimmed = query.trim()
  if (!OpenLocationCode.isFull(trimmed)) return null
  const area = OpenLocationCode.decode(trimmed)
  let zoom = 16
  if (area.codeLength <= 4) zoom = 5
  else if (area.codeLength <= 6) zoom = 9
  else if (area.codeLength <= 8) zoom = 13
  return { lat: area.latitudeCenter, lng: area.longitudeCenter, zoom }
}

async function coordsFromLongPlusCode(
  query: string,
): Promise<DirectCoords | null> {
  const match = query.match(shortPlusCodePattern)
  if (!match) return null
  const [, shortCode, location] = match

  let refLat: number, refLng: number
  try {
    const parts = location.split(',')
    let results = null
    while (parts.length > 0) {
      results = await nominatimSearch(parts.join(',').trim(), 1)
      if (results?.length) break
      parts.shift()
    }
    if (!results?.length) return null
    refLat = parseFloat(results[0].lat)
    refLng = parseFloat(results[0].lon)
  } catch {
    return null
  }

  try {
    const fullCode = OpenLocationCode.recoverNearest(shortCode, refLat, refLng)
    const area = OpenLocationCode.decode(fullCode)
    return { lat: area.latitudeCenter, lng: area.longitudeCenter, zoom: 16 }
  } catch {
    return null
  }
}
