"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.

This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import datetime
import os
import platform
import requests
import shutil
import threading
from pathlib import Path
from time import monotonic

from openpilot.common.params import Params
from openpilot.selfdrive.ui.ui_state import device, ui_state
from openpilot.selfdrive.ui.layouts.settings.software import time_ago
from openpilot.system.hardware.hw import Paths
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import DialogResult, Widget
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.widgets.list_view import text_item
from openpilot.system.ui.widgets.scroller_tici import Scroller

from openpilot.system.ui.sunnypilot.lib.utils import NoElideButtonAction
from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP
from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeFolder, TreeNode, TreeOptionDialog
from openpilot.system.ui.sunnypilot.widgets.progress_bar import progress_item

MAP_PATH = Path(Paths.mapd_root()) / "offline"


class OSMLayout(Widget):
  def __init__(self):
    super().__init__()
    self._current_percent = 0
    self._last_map_size_update = 0
    self._mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else ui_state.params
    self._initialize_items()
    self._update_map_size()
    self._progress.set_visible(False)
    self._state_btn.set_visible(False)
    self._mapd_version.action_item.set_text(ui_state.params.get("MapdVersion") or "Loading...")
    self._scroller = Scroller(self.items, line_separator=True, spacing=0)

  def _initialize_items(self):
    self._mapd_version = text_item(tr("Mapd Version"), lambda: ui_state.params.get("MapdVersion") or "Loading...")
    self._delete_maps_btn = ListItemSP(tr("Downloaded Maps"), action_item=NoElideButtonAction(tr("DELETE"), enabled=True), callback=self._delete_maps)
    self._progress = progress_item(tr("Downloading Map"))
    self._update_btn = ListItemSP(tr("Database Update"), action_item=NoElideButtonAction(tr("CHECK"), enabled=True), callback=self._update_db)
    self._country_btn = ListItemSP(tr("Country"), action_item=NoElideButtonAction(tr("SELECT"), enabled=True), callback=lambda: self._select_region("Country"))
    self._state_btn = ListItemSP(tr("State"), action_item=NoElideButtonAction(tr("SELECT"), enabled=True), callback=lambda: self._select_region("State"))

    self.items = [self._mapd_version, self._delete_maps_btn, self._progress, self._update_btn, self._country_btn, self._state_btn]

  def _show_confirm(self, msg, confirm_text, func):
    gui_app.push_widget(ConfirmDialog(msg, confirm_text, callback=lambda res: func() if res == DialogResult.CONFIRM else None))

  def calculate_size(self):
    total_size = 0
    directories_to_scan = [MAP_PATH] if MAP_PATH.exists() else []
    while directories_to_scan:
      try:
        for entry in os.scandir(directories_to_scan.pop()):
          if entry.is_file():
            total_size += entry.stat().st_size
          elif entry.is_dir():
            directories_to_scan.append(entry.path)
      except OSError:
        pass
    self._delete_maps_btn.action_item.set_value(f"{total_size / 1024 ** 2:.2f} MB" if total_size < 1024 ** 3 else f"{total_size / 1024 ** 3:.2f} GB")

  def _update_map_size(self):
    threading.Thread(target=self.calculate_size, daemon=True).start()

  def _do_delete_maps(self):
    if MAP_PATH.exists():
      shutil.rmtree(MAP_PATH)

    for param in ("OsmDownloadedDate", "OsmLocal", "OsmLocationName", "OsmLocationTitle", "OsmStateName", "OsmStateTitle"):
      ui_state.params.remove(param)

    self._delete_maps_btn.action_item.set_enabled(True)
    self._delete_maps_btn.action_item.set_text(tr("DELETE"))
    self._update_map_size()

  def _on_confirm_delete_maps(self):
    self._delete_maps_btn.action_item.set_enabled(False)
    self._delete_maps_btn.action_item.set_text("DELETING...")
    threading.Thread(target=self._do_delete_maps).start()

  def _delete_maps(self):
    self._show_confirm(tr("This will delete ALL downloaded maps\n\nAre you sure you want to delete all maps?"),
                       tr("Yes, delete all maps"), self._on_confirm_delete_maps)

  def _update_db(self):
    self._show_confirm(tr("This will start the download process and it might take a while to complete."), tr("Start Download"),
                       lambda: ui_state.params.put_bool("OsmDbUpdatesCheck", True))

  def _select_region(self, region_type):
    is_country = region_type == "Country"
    btn = self._country_btn if is_country else self._state_btn
    btn.action_item.set_enabled(False)
    btn.action_item.set_text(tr("FETCHING..."))
    threading.Thread(target=self._do_select_region, args=(region_type, btn)).start()

  def _handle_region_selection(self, region_type, locations, key, res, ref):
    if res != DialogResult.CONFIRM or not ref:
      if region_type == "State" and res == DialogResult.CANCEL:
        if ui_state.params.get("OsmLocationName") == "US" and not ui_state.params.get("OsmStateName"):
          ui_state.params.remove("OsmLocationName")
          ui_state.params.remove("OsmLocationTitle")
          ui_state.params.remove("OsmLocal")
          self._update_labels()
      return

    if region_type == "Country":
      ui_state.params.put_bool("OsmLocal", True)
      ui_state.params.remove("OsmStateName")
      ui_state.params.remove("OsmStateTitle")

    ui_state.params.put(f"{key}Name", ref)
    name = next((n.data['display_name'] for n in locations if n.ref == ref), ref)
    ui_state.params.put(f"{key}Title", name)

    if ref == "US" and region_type == "Country":
      self._select_region("State")
    else:
      self._update_db()

  def _do_select_region(self, region_type, btn):
    base_url = "https://raw.githubusercontent.com/pfeiferj/openpilot-mapd/main/"
    url = base_url + ("nation_bounding_boxes.json" if region_type == "Country" else "us_states_bounding_boxes.json")
    try:
      data = requests.get(url, timeout=10).json()
      locations = sorted([TreeNode(ref=k, data={'display_name': v['full_name']}) for k, v in data.items()], key=lambda n: n.data['display_name'])
    except Exception:
      locations = []

    if region_type == "State":
      locations.insert(0, TreeNode(ref="All", data={'display_name': tr("All states (~6.0 GB)")}))

    btn.action_item.set_enabled(True)
    btn.action_item.set_text(tr("SELECT"))

    key = "OsmLocation" if region_type == "Country" else "OsmState"
    current = ui_state.params.get(f"{key}Name") or ""

    dialog = TreeOptionDialog(tr(f"Select {region_type}"), [TreeFolder(folder="", nodes=locations)], current_ref=current, search_prompt="Perform a search")
    dialog.on_exit = lambda res: self._handle_region_selection(region_type, locations, key, res, dialog.selection_ref)
    gui_app.push_widget(dialog)

  def _update_labels(self):
    downloading = bool(self._mem_params.get("OSMDownloadLocations"))
    self._country_btn.set_enabled(not downloading)
    self._state_btn.set_enabled(not downloading)
    self._state_btn.set_visible(ui_state.params.get("OsmLocationName") == "US")
    self._update_btn.set_visible(bool(ui_state.params.get("OsmLocationName")))

    self._country_btn.action_item.set_value(ui_state.params.get("OsmLocationTitle") or "")
    self._state_btn.action_item.set_value(ui_state.params.get("OsmStateTitle") or "")

    pending = ui_state.params.get_bool("OsmDbUpdatesCheck")
    if downloading or pending:
      if downloading:
        device._reset_interactive_timeout()
        self._update_map_size()
      self._progress.set_visible(True)
      progress = ui_state.params.get("OSMDownloadProgress")
      total = progress.get('total_files', 0) if progress else 0
      done = progress.get('downloaded_files', 0) if progress else 0
      failed = total > 0 and not downloading and done < total

      if total > 0:
        progress_perc = max(0.0, min(100.0, (done / total) * 100.0))
      else:
        progress_perc = 0.0

      if failed:
        text = "0% - Downloading Maps"
        btn_text = tr("Error: Invalid download. Retry.")
        self._current_percent = 0.0
      elif total > 0 and downloading:
        self._current_percent = progress_perc
        perc_int = int(progress_perc)
        text = f"{perc_int}% - Downloading Maps"
        btn_text = f"{done}/{total} ({perc_int}%)"
      else:
        self._current_percent = 0.0
        text = "0% - Downloading Maps"
        btn_text = tr("Downloading Maps...")

      self._progress.action_item.update(self._current_percent, text, show_progress=total > 0 and downloading and not failed)
      self._update_btn.action_item.set_enabled(not downloading)  # TODO-SP: introduce CANCEL database download with mapd
      self._update_btn.action_item.set_value(btn_text)
      self._country_btn.action_item.set_enabled(not downloading)
      self._state_btn.action_item.set_enabled(not downloading)
      self._delete_maps_btn.action_item.set_enabled(not downloading)
    else:
      self._progress.set_visible(False)
      self._update_btn.action_item.set_enabled(True)
      self._country_btn.action_item.set_enabled(True)
      self._state_btn.action_item.set_enabled(True)
      self._delete_maps_btn.action_item.set_enabled(True)

      ts = ui_state.params.get("OsmDownloadedDate")
      dt: datetime.datetime | None = None

      if ts:
        try:
          ts_f = float(ts)
          if ts_f > 0:
            dt = datetime.datetime.fromtimestamp(ts_f, tz=datetime.UTC)
        except (ValueError, TypeError):
          dt = None

      formatted = time_ago(dt)
      self._update_btn.action_item.set_value(tr("Last checked {}").format(formatted))

  def show_event(self):
    self._scroller.show_event()

  def _update_state(self):
    now = monotonic()
    if now - self._last_map_size_update >= 1.0:
      self._last_map_size_update = now
      self._update_labels()

  def _render(self, rect):
    self._scroller.render(rect)
