// Copyright 2018-2026 the Deno authors. MIT license.

use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use std::path::Prefix;
use std::str::FromStr;

use deno_config::UrlToFilePathError;
use deno_core::error::AnyError;
use deno_core::url::Position;
use deno_core::url::Url;
use deno_path_util::url_to_file_path;
use lsp_types::Uri;

use super::logging::lsp_warn;

pub fn uri_parse_unencoded(s: &str) -> Result<Uri, AnyError> {
  url_to_uri(&Url::parse(s)?)
}

pub fn normalize_uri(uri: &Uri) -> Uri {
  if !uri.scheme().as_str().eq_ignore_ascii_case("file") {
    return normalize_non_file_uri(uri);
  }
  let Some(path) = uri.to_file_path() else {
    return normalize_non_file_uri(uri);
  };
  let normalized_path = normalize_path(path);
  let mut encoded_path =
    fluent_uri::pct_enc::EString::<fluent_uri::pct_enc::encoder::Path>::new();
  let mut path_only_has_prefix = false;
  for component in normalized_path.components() {
    match component {
      Component::Prefix(prefix) => {
        path_only_has_prefix = true;
        match prefix.kind() {
          Prefix::Disk(mut letter) | Prefix::VerbatimDisk(mut letter) => {
            encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>("/");
            letter.make_ascii_uppercase();
            let b = [letter];
            // SAFETY: Drive letter is ascii.
            let s = unsafe { str::from_utf8_unchecked(&b) };
            encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>(s);
            encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>(":");
          }
          Prefix::UNC(..) | Prefix::VerbatimUNC(..) => {
            // These should be carried in `uri.authority()`.
          }
          Prefix::Verbatim(_) | Prefix::DeviceNS(_) => {
            // Not a local path, abort.
            return normalize_non_file_uri(uri);
          }
        }
      }
      Component::RootDir => {}
      component => {
        path_only_has_prefix = false;
        encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>("/");
        encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>(
          &component.as_os_str().to_string_lossy(),
        );
      }
    }
  }
  if encoded_path.is_empty() || path_only_has_prefix {
    encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>("/");
  }
  fluent_uri::Uri::builder()
    .scheme(fluent_uri::component::Scheme::new_or_panic("file"))
    .optional(fluent_uri::build::Builder::authority, uri.authority())
    .path(encoded_path.as_ref())
    .optional(fluent_uri::build::Builder::query, uri.query())
    .optional(fluent_uri::build::Builder::fragment, uri.fragment())
    .build()
    .expect("component constraints should be met by the above")
    .normalize()
    .into()
}

// TODO(nayeemrmn): Remove whe `Uri::normalize()` is fixed:
// https://github.com/yescallop/fluent-uri-rs/issues/45
pub fn normalize_non_file_uri(uri: &Uri) -> Uri {
  let mut encoded_path =
    fluent_uri::pct_enc::EString::<fluent_uri::pct_enc::encoder::Path>::new();
  encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>(
    &percent_encoding::percent_decode_str(uri.path().as_str())
      .decode_utf8_lossy(),
  );
  fluent_uri::Uri::builder()
    .scheme(uri.scheme())
    .optional(fluent_uri::build::Builder::authority, uri.authority())
    .path(encoded_path.as_ref())
    .optional(fluent_uri::build::Builder::query, uri.query())
    .optional(fluent_uri::build::Builder::fragment, uri.fragment())
    .build()
    .expect("component constraints should be met by the above")
    .into()
}

pub fn url_to_uri(url: &Url) -> Result<Uri, AnyError> {
  let uri_before_path = Uri::from_str(&url[..Position::BeforePath])
    .inspect_err(|err| {
      lsp_warn!("Could not convert URL \"{url}\" to URI: {err}")
    })?;
  let mut encoded_path =
    fluent_uri::pct_enc::EString::<fluent_uri::pct_enc::encoder::Path>::new();
  encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>(
    &percent_encoding::percent_decode_str(url.path()).decode_utf8_lossy(),
  );
  let encoded_query = url.query().map(|query| {
    let mut encoded_query = fluent_uri::pct_enc::EString::<
      fluent_uri::pct_enc::encoder::Query,
    >::new();
    encoded_query.encode_str::<fluent_uri::pct_enc::encoder::Query>(query);
    encoded_query
  });
  let encoded_fragment = url.fragment().map(|fragment| {
    let mut encoded_fragment = fluent_uri::pct_enc::EString::<
      fluent_uri::pct_enc::encoder::Fragment,
    >::new();
    encoded_fragment
      .encode_str::<fluent_uri::pct_enc::encoder::Fragment>(fragment);
    encoded_fragment
  });
  let uri = fluent_uri::Uri::builder()
    .scheme(uri_before_path.scheme())
    .optional(
      fluent_uri::build::Builder::authority,
      uri_before_path.authority(),
    )
    .path(encoded_path.as_ref())
    .optional(fluent_uri::build::Builder::query, encoded_query.as_deref())
    .optional(
      fluent_uri::build::Builder::fragment,
      encoded_fragment.as_deref(),
    )
    .build()
    .expect("component constraints should be met by the above")
    .into();
  Ok(normalize_uri(&uri))
}

pub fn uri_to_url(uri: &Uri) -> Url {
  (|| {
    let scheme = uri.scheme();
    if !scheme.as_str().eq_ignore_ascii_case("untitled")
      && !scheme.as_str().eq_ignore_ascii_case("vscode-notebook-cell")
      && !scheme.as_str().eq_ignore_ascii_case("deno-notebook-cell")
      && !scheme.as_str().eq_ignore_ascii_case("vscode-userdata")
    {
      return None;
    }
    let mut s = String::with_capacity(uri.as_str().len());
    s.push_str("file:///");
    s.push_str(uri.path().as_str().trim_start_matches('/'));
    if let Some(query) = uri.query() {
      s.push('?');
      s.push_str(query.as_str());
    }
    if let Some(fragment) = uri.fragment() {
      s.push('#');
      s.push_str(fragment.as_str());
    }
    Url::parse(&s).ok().map(normalize_url)
  })()
  .unwrap_or_else(|| {
    normalize_url(
      Url::parse(uri.as_str())
        .inspect_err(|err| {
          lsp_warn!("Couldn't parse uri \"{}\" as url: {err}", uri.as_str());
        })
        .unwrap(),
    )
  })
}

pub fn uri_to_file_path(uri: &Uri) -> Result<PathBuf, UrlToFilePathError> {
  url_to_file_path(&uri_to_url(uri))
}

pub fn uri_is_file_like(uri: &Uri) -> bool {
  let scheme = uri.scheme();
  scheme.as_str().eq_ignore_ascii_case("file")
    || scheme.as_str().eq_ignore_ascii_case("untitled")
    || scheme.as_str().eq_ignore_ascii_case("vscode-notebook-cell")
    || scheme.as_str().eq_ignore_ascii_case("deno-notebook-cell")
    || scheme.as_str().eq_ignore_ascii_case("vscode-userdata")
}

/// Whether the URI refers to an in-memory editor document that isn't backed by
/// a file on disk yet: untitled files and unsaved (untitled) notebook cells.
/// These convert to a `file:` URL at the filesystem root via [`uri_to_url`], so
/// they match no workspace scope and need to be associated with the workspace
/// root instead.
pub fn uri_is_in_memory(uri: &Uri) -> bool {
  let scheme = uri.scheme();
  scheme.as_str().eq_ignore_ascii_case("untitled")
    || scheme.as_str().eq_ignore_ascii_case("vscode-notebook-cell")
    || scheme.as_str().eq_ignore_ascii_case("deno-notebook-cell")
}

fn normalize_url(url: Url) -> Url {
  let Ok(path) = url_to_file_path(&url) else {
    return url;
  };
  let normalized_path = normalize_path(&path);
  let Ok(mut normalized_url) = Url::from_file_path(&normalized_path) else {
    return url;
  };
  if let Some(query) = url.query() {
    normalized_url.set_query(Some(query));
  }
  if let Some(fragment) = url.fragment() {
    normalized_url.set_fragment(Some(fragment));
  }
  normalized_url
}

// TODO(nayeemrmn): Change the version of this in deno_path_util to force
// uppercase on drive letters. Then remove this.
pub fn normalize_path<P: AsRef<Path>>(path: P) -> PathBuf {
  fn inner(path: &Path) -> PathBuf {
    let mut components = path.components().peekable();
    let mut ret =
      if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
        components.next();
        let s = c.as_os_str();
        if s.len() == 2 {
          PathBuf::from(s.to_ascii_uppercase())
        } else {
          PathBuf::from(s)
        }
      } else {
        PathBuf::new()
      };

    for component in components {
      match component {
        Component::Prefix(..) => unreachable!(),
        Component::RootDir => {
          ret.push(component.as_os_str());
        }
        Component::CurDir => {}
        Component::ParentDir => {
          ret.pop();
        }
        Component::Normal(c) => {
          ret.push(c);
        }
      }
    }
    ret
  }

  inner(path.as_ref())
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn test_normalize_non_file_uri_no_double_encode_fragment() {
    // Notebook cell URIs have percent-encoded fragments (base64 padding).
    let uri: Uri =
      "vscode-notebook-cell:/Users/test/scratch.ipynb#W1sZmlsZQ%3D%3D"
        .parse()
        .unwrap();
    let normalized = normalize_non_file_uri(&uri);
    assert_eq!(
      normalized.as_str(),
      "vscode-notebook-cell:/Users/test/scratch.ipynb#W1sZmlsZQ%3D%3D"
    );
    // Ensure idempotency.
    let normalized2 = normalize_non_file_uri(&normalized);
    assert_eq!(normalized.as_str(), normalized2.as_str());
  }

  #[test]
  fn test_normalize_uri_notebook_cell_idempotent() {
    let uri: Uri =
      "vscode-notebook-cell:/Users/test/scratch.ipynb#W0sZmlsZQ%3D%3D.ts"
        .parse()
        .unwrap();
    let n1 = normalize_uri(&uri);
    let n2 = normalize_uri(&n1);
    assert_eq!(n1.as_str(), n2.as_str());
  }

  #[test]
  fn test_normalize_non_file_uri_no_double_encode_query() {
    let uri: Uri = "custom-scheme:/path?key=val%20ue".parse().unwrap();
    let normalized = normalize_non_file_uri(&uri);
    assert_eq!(normalized.as_str(), "custom-scheme:/path?key=val%20ue");
    let normalized2 = normalize_non_file_uri(&normalized);
    assert_eq!(normalized.as_str(), normalized2.as_str());
  }
}
