///|
pub struct LoginResult {
  code : String
}

///|
pub struct StorageResult {
  data : Json
}

///|
pub struct LocationResult {
  latitude : Double
  longitude : Double
  speed : Double?
  accuracy : Double?
  altitude : Double?
  vertical_accuracy : Double?
  horizontal_accuracy : Double?
}

///|
pub struct MediaFile {
  temp_file_path : String
  size : Int?
  file_type : String?
  width : Int?
  height : Int?
  duration : Double?
  thumb_temp_file_path : String?
}

///|
pub struct ChooseMediaResult {
  temp_files : Array[MediaFile]
  media_type : String?
}

///|
pub struct PaymentParams {
  time_stamp : String
  nonce_str : String
  package_value : String
  sign_type : String
  pay_sign : String
}

///|
pub fn PaymentParams::new(
  time_stamp~ : String,
  nonce_str~ : String,
  package_value~ : String,
  sign_type~ : String,
  pay_sign~ : String,
) -> PaymentParams {
  { time_stamp, nonce_str, package_value, sign_type, pay_sign, }
}

///|
pub(all) enum Capability {
  Login
  GetStorage
  SetStorage
  Request
  ShowToast
  GetLocation
  ChooseMedia
  RequestPayment
  NavigateTo
  RedirectTo
  SwitchTab
  NavigateBack
  MeasureNodes
} derive(Debug, Eq)

///|
pub fn Capability::name(self : Capability) -> String {
  match self {
    Login => "wx.login"
    GetStorage => "wx.getStorage"
    SetStorage => "wx.setStorage"
    Request => "wx.request"
    ShowToast => "wx.showToast"
    GetLocation => "wx.getLocation"
    ChooseMedia => "wx.chooseMedia"
    RequestPayment => "wx.requestPayment"
    NavigateTo => "wx.navigateTo"
    RedirectTo => "wx.redirectTo"
    SwitchTab => "wx.switchTab"
    NavigateBack => "wx.navigateBack"
    MeasureNodes => "minimoon.measureNodes"
  }
}

///|
fn Capability::to_core(self : Capability) -> @renderer.MiniappCapability {
  @renderer.miniapp_capability(self.name())
}

///|
pub(all) enum HostErrorKind {
  Failed
  Unavailable
  InvalidPayload
} derive(Debug, Eq)

///|
pub struct HostError {
  capability : Capability
  kind : HostErrorKind
  message : String
  code : String?
  raw : Json
}

///|
fn error_kind(phase : String) -> HostErrorKind {
  match phase {
    "unavailable" => Unavailable
    "invalid_payload" => InvalidPayload
    _ => Failed
  }
}

///|
fn raw_json(raw : String) -> Json {
  @json.parse(raw) catch {
    _ => Json::null()
  }
}

///|
fn json_code(raw : Json) -> String? {
  match raw {
    Object(fields) =>
      match fields.get("errCode") {
        Some(String(value)) => Some(value)
        Some(Number(value, ..)) => Some(value.to_string())
        _ => None
      }
    _ => None
  }
}

///|
fn host_error(
  capability : Capability,
  kind : HostErrorKind,
  message : String,
  raw : Json,
) -> HostError {
  { capability, kind, message, code: json_code(raw), raw, }
}

///|
fn invalid_payload(
  capability : Capability,
  message : String,
  raw : Json,
) -> HostError {
  host_error(capability, InvalidPayload, message, raw)
}

///|
fn[A] host_effect(
  capability : Capability,
  payload : Json,
  decode : (Json) -> Result[A, HostError],
  resolve : Emit[Result[A, HostError]],
) -> Cmd {
  Cmd(
    @val.host_effect(capability.name(), payload.stringify(), outcome => {
      match outcome {
        @val.HostOk(raw) => {
          let parsed : Json? = Some(@json.parse(raw)) catch { _ => None }
          resolve(
            match parsed {
              Some(value) => decode(value)
              None =>
                Err(
                  invalid_payload(
                    capability,
                    "host success payload is not valid JSON",
                    Json::string(raw),
                  ),
                )
            },
          ).0
        }
        @val.HostErr(phase, message, raw) => {
          let value = raw_json(raw)
          resolve(
            Err(host_error(capability, error_kind(phase), message, value)),
          ).0
        }
      }
    }),
  )
}

///|
fn object_result(
  capability : Capability,
  value : Json,
) -> Result[Map[String, Json], HostError] {
  match value {
    Object(fields) => Ok(fields)
    _ =>
      Err(invalid_payload(capability, "host result must be an object", value))
  }
}

///|
fn string_field(fields : Map[String, Json], name : String) -> String? {
  match fields.get(name) {
    Some(String(value)) => Some(value)
    _ => None
  }
}

///|
fn number_field(fields : Map[String, Json], name : String) -> Double? {
  match fields.get(name) {
    Some(Number(value, ..)) => Some(value)
    _ => None
  }
}

///|
fn host_int32(value : Double) -> Int? {
  if value < 0.0 || value > 2147483647.0 {
    return None
  }
  let integer = value.to_int()
  if integer.to_double() == value {
    Some(integer)
  } else {
    None
  }
}

///|
fn decode_login(value : Json) -> Result[LoginResult, HostError] {
  let capability = Login
  match object_result(capability, value) {
    Err(error) => Err(error)
    Ok(fields) =>
      match string_field(fields, "code") {
        Some(code) if code != "" => Ok({ code, })
        _ =>
          Err(
            invalid_payload(
              capability, "wx.login result.code is missing", value,
            ),
          )
      }
  }
}

///|
fn decode_storage(value : Json) -> Result[StorageResult, HostError] {
  let capability = GetStorage
  match object_result(capability, value) {
    Err(error) => Err(error)
    Ok(fields) =>
      match fields.get("data") {
        Some(data) => Ok({ data, })
        None =>
          Err(
            invalid_payload(
              capability, "wx.getStorage result.data is missing", value,
            ),
          )
      }
  }
}

///|
fn decode_location(value : Json) -> Result[LocationResult, HostError] {
  let capability = GetLocation
  match object_result(capability, value) {
    Err(error) => Err(error)
    Ok(fields) =>
      match
        (number_field(fields, "latitude"), number_field(fields, "longitude")) {
        (Some(latitude), Some(longitude)) =>
          Ok({
            latitude,
            longitude,
            speed: number_field(fields, "speed"),
            accuracy: number_field(fields, "accuracy"),
            altitude: number_field(fields, "altitude"),
            vertical_accuracy: number_field(fields, "verticalAccuracy"),
            horizontal_accuracy: number_field(fields, "horizontalAccuracy"),
          })
        _ =>
          Err(
            invalid_payload(
              capability, "wx.getLocation latitude/longitude are missing", value,
            ),
          )
      }
  }
}

///|
fn optional_media_int(
  fields : Map[String, Json],
  name : String,
) -> Result[Int?, Unit] {
  match fields.get(name) {
    None => Ok(None)
    Some(Number(value, ..)) =>
      match host_int32(value) {
        Some(integer) => Ok(Some(integer))
        None => Err(())
      }
    Some(_) => Err(())
  }
}

///|
fn media_file(value : Json) -> MediaFile? {
  match value {
    Object(fields) =>
      match string_field(fields, "tempFilePath") {
        Some(temp_file_path) => {
          let size = match optional_media_int(fields, "size") {
            Ok(value) => value
            Err(_) => return None
          }
          let width = match optional_media_int(fields, "width") {
            Ok(value) => value
            Err(_) => return None
          }
          let height = match optional_media_int(fields, "height") {
            Ok(value) => value
            Err(_) => return None
          }
          Some({
            temp_file_path,
            size,
            file_type: string_field(fields, "fileType"),
            width,
            height,
            duration: number_field(fields, "duration"),
            thumb_temp_file_path: string_field(fields, "thumbTempFilePath"),
          })
        }
        None => None
      }
    _ => None
  }
}

///|
fn decode_media(value : Json) -> Result[ChooseMediaResult, HostError] {
  let capability = ChooseMedia
  match object_result(capability, value) {
    Err(error) => Err(error)
    Ok(fields) =>
      match fields.get("tempFiles") {
        Some(Array(values)) => {
          let temp_files = values.filter_map(media_file)
          if temp_files.length() != values.length() {
            Err(
              invalid_payload(
                capability, "wx.chooseMedia tempFiles are malformed", value,
              ),
            )
          } else {
            Ok({ temp_files, media_type: string_field(fields, "type"), })
          }
        }
        _ =>
          Err(
            invalid_payload(
              capability, "wx.chooseMedia tempFiles are missing", value,
            ),
          )
      }
  }
}

///|
fn decode_unit(_value : Json) -> Result[Unit, HostError] {
  Ok(())
}

///|
pub fn login(resolve : Emit[Result[LoginResult, HostError]]) -> Cmd {
  host_effect(Login, Json::empty_object(), decode_login, resolve)
}

///|
pub fn get_storage(
  key : String,
  resolve : Emit[Result[StorageResult, HostError]],
) -> Cmd {
  host_effect(
    GetStorage,
    Json::object({ "key": Json::string(key) }),
    decode_storage,
    resolve,
  )
}

///|
pub fn set_storage(
  key : String,
  value : Json,
  resolve : Emit[Result[Unit, HostError]],
) -> Cmd {
  host_effect(
    SetStorage,
    Json::object({ "key": Json::string(key), "data": value }),
    decode_unit,
    resolve,
  )
}

///|
pub fn show_toast(
  title : String,
  resolve : Emit[Result[Unit, HostError]],
) -> Cmd {
  host_effect(
    ShowToast,
    Json::object({ "title": Json::string(title) }),
    decode_unit,
    resolve,
  )
}

///|
pub fn get_location(resolve : Emit[Result[LocationResult, HostError]]) -> Cmd {
  host_effect(GetLocation, Json::empty_object(), decode_location, resolve)
}

///|
pub fn choose_media(
  resolve : Emit[Result[ChooseMediaResult, HostError]],
) -> Cmd {
  host_effect(ChooseMedia, Json::empty_object(), decode_media, resolve)
}

///|
pub fn request_payment(
  params : PaymentParams,
  resolve : Emit[Result[Unit, HostError]],
) -> Cmd {
  host_effect(
    RequestPayment,
    Json::object({
      "timeStamp": Json::string(params.time_stamp),
      "nonceStr": Json::string(params.nonce_str),
      "package": Json::string(params.package_value),
      "signType": Json::string(params.sign_type),
      "paySign": Json::string(params.pay_sign),
    }),
    decode_unit,
    resolve,
  )
}

///|
pub fn navigate_to(target : Route) -> Cmd {
  Cmd(@val.navigate_to(target.url()))
}

///|
pub fn redirect_to(target : Route) -> Cmd {
  Cmd(@val.redirect_to(target.url()))
}

///|
/// Switch to a configured native Tab page. Native Tab routes have no query.
pub fn switch_tab(target : Route) -> Cmd {
  guard !target.has_query() else {
    abort("switchTab route must not contain query parameters")
  }
  Cmd(@val.switch_tab(target.url()))
}

///|
pub fn navigate_back(delta? : Int = 1) -> Cmd {
  Cmd(@val.navigate_back(delta))
}

///|
pub fn navigate_back_or(fallback~ : Route, delta? : Int = 1) -> Cmd {
  Cmd(@val.navigate_back_or_redirect(delta, fallback.url()))
}