/* -*- Mode: rust; rust-indent-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

extern crate url;
use url::{Url, ParseOptions, Position};
use url::quirks;

extern crate nsstring;
use nsstring::{nsCString, nsACString};

extern crate nserror;
use nserror::*;

extern crate xpcom;
use xpcom::{AtomicRefcnt, RefPtr, RefCounted};
use xpcom::interfaces::nsrefcnt;

use std::str;
use std::ptr;
use std::ops;
use std::marker::PhantomData;
use std::fmt::Write;

/// Helper macro. If the expression $e is Ok(t) evaluates to t, otherwise,
/// returns NS_ERROR_MALFORMED_URI.
macro_rules! try_or_malformed {
  ($e:expr) => (
    match $e {
      Ok(v) => v,
      Err(_) => return NS_ERROR_MALFORMED_URI,
    }
  )
}

fn parser<'a>() -> ParseOptions<'a> {
  Url::options()
}

fn default_port(scheme: &str) -> Option<u16> {
  match scheme {
    "ftp" => Some(21),
    "gopher" => Some(70),
    "http" => Some(80),
    "https" => Some(443),
    "ws" => Some(80),
    "wss" => Some(443),
    "rtsp" => Some(443),
    "moz-anno" => Some(443),
    "android" => Some(443),
    _ => None,
  }
}

/// A slice into the backing string. This type is only valid as long as the
/// MozURL which it was pulled from is valid. In C++, this type implicitly
/// converts to a nsDependentCString, and is an implementation detail.
///
/// This type exists because, unlike &str, this type is safe to return over FFI.
#[repr(C)]
pub struct SpecSlice<'a> {
  data: *const u8,
  len: u32,
  _marker: PhantomData<&'a [u8]>,
}

impl<'a> From<&'a str> for SpecSlice<'a> {
  fn from(s: &'a str) -> SpecSlice<'a> {
    assert!(s.len() < u32::max_value() as usize);
    SpecSlice {
      data: s.as_ptr(),
      len: s.len() as u32,
      _marker: PhantomData,
    }
  }
}

/// The MozURL reference-counted threadsafe URL type. This type intentionally
/// implements no XPCOM interfaces, and all method calls are non-virtual.
#[repr(C)]
pub struct MozURL {
  pub url: Url,
  refcnt: AtomicRefcnt,
}

impl MozURL {
  pub fn from_url(url: Url) -> RefPtr<MozURL> {
    // Actually allocate the URL on the heap. This is the only place we actually
    // create a MozURL, other than in clone().
    unsafe {
      RefPtr::from_raw(Box::into_raw(Box::new(MozURL {
        url: url,
        refcnt: AtomicRefcnt::new(),
      }))).unwrap()
    }
  }
}

impl ops::Deref for MozURL {
  type Target = Url;
  fn deref(&self) -> &Url {
    &self.url
  }
}
impl ops::DerefMut for MozURL {
  fn deref_mut(&mut self) -> &mut Url {
    &mut self.url
  }
}

// Memory Management for MozURL
#[no_mangle]
pub unsafe extern "C" fn mozurl_addref(url: &MozURL) -> nsrefcnt {
  url.refcnt.inc()
}

#[no_mangle]
pub unsafe extern "C" fn mozurl_release(url: &MozURL) -> nsrefcnt {
  let rc = url.refcnt.dec();
  if rc == 0 {
    Box::from_raw(url as *const MozURL as *mut MozURL);
  }
  rc
}

// xpcom::RefPtr support
unsafe impl RefCounted for MozURL {
  unsafe fn addref(&self) {
    mozurl_addref(self);
  }
  unsafe fn release(&self) {
    mozurl_release(self);
  }
}

// Allocate a new MozURL object with a RefCnt of 1, and store a pointer to it
// into url.
#[no_mangle]
pub extern "C" fn mozurl_new(
  result: &mut *const MozURL,
  spec: &nsACString,
  base: Option<&MozURL>,
) -> nsresult {
  *result = ptr::null_mut();

  let spec = try_or_malformed!(str::from_utf8(spec));
  let url = if let Some(base) = base {
    try_or_malformed!(base.url.join(spec))
  } else {
    try_or_malformed!(parser().parse(spec))
  };

  MozURL::from_url(url).forget(result);
  NS_OK
}

/// Allocate a new MozURL object which is a clone of the original, and store a
/// pointer to it into newurl.
#[no_mangle]
pub extern "C" fn mozurl_clone(url: &MozURL, newurl: &mut *const MozURL) {
  MozURL::from_url(url.url.clone()).forget(newurl);
}

#[no_mangle]
pub extern "C" fn mozurl_spec(url: &MozURL) -> SpecSlice {
  url.as_ref().into()
}

#[no_mangle]
pub extern "C" fn mozurl_scheme(url: &MozURL) -> SpecSlice {
  url.scheme().into()
}

#[no_mangle]
pub extern "C" fn mozurl_username(url: &MozURL) -> SpecSlice {
  if url.cannot_be_a_base() {
    "".into()
  } else {
    url.username().into()
  }
}

#[no_mangle]
pub extern "C" fn mozurl_password(url: &MozURL) -> SpecSlice {
  url.password().unwrap_or("").into()
}

#[no_mangle]
pub extern "C" fn mozurl_host(url: &MozURL) -> SpecSlice {
  url.host_str().unwrap_or("").into()
}

#[no_mangle]
pub extern "C" fn mozurl_port(url: &MozURL) -> i32 {
  // NOTE: Gecko uses -1 to represent the default port.
  url.port().map(|p| p as i32).unwrap_or(-1)
}

#[no_mangle]
pub extern "C" fn mozurl_host_port(url: &MozURL) -> SpecSlice {
  (&url[Position::BeforeHost..Position::BeforePath]).into()
}

#[no_mangle]
pub extern "C" fn mozurl_filepath(url: &MozURL) -> SpecSlice {
  url.path().into()
}

#[no_mangle]
pub extern "C" fn mozurl_path(url: &MozURL) -> SpecSlice {
  (&url[Position::BeforePath..]).into()
}

#[no_mangle]
pub extern "C" fn mozurl_query(url: &MozURL) -> SpecSlice {
  url.query().unwrap_or("").into()
}

#[no_mangle]
pub extern "C" fn mozurl_fragment(url: &MozURL) -> SpecSlice {
  url.fragment().unwrap_or("").into()
}

#[no_mangle]
pub extern "C" fn mozurl_has_fragment(url: &MozURL) -> bool {
  url.fragment().is_some()
}

#[no_mangle]
pub extern "C" fn mozurl_origin(url: &MozURL, origin: &mut nsACString) {
  // NOTE: Try to re-use the allocation we got from rust-url, and transfer
  // ownership of the buffer to C++.
  let mut o = nsCString::from(url.origin().ascii_serialization());
  origin.take_from(&mut o);
}

// Helper macro for debug asserting that we're the only reference to MozURL.
macro_rules! debug_assert_mut {
  ($e:expr) => {
    debug_assert_eq!(
      $e.refcnt.get(), 1,
      "Cannot mutate an aliased MozURL!"
    );
  }
}

#[no_mangle]
pub extern "C" fn mozurl_set_scheme(url: &mut MozURL, scheme: &nsACString) -> nsresult {
  debug_assert_mut!(url);
  let scheme = try_or_malformed!(str::from_utf8(scheme));
  try_or_malformed!(quirks::set_protocol(url, scheme));
  NS_OK
}

#[no_mangle]
pub extern "C" fn mozurl_set_username(url: &mut MozURL, username: &nsACString) -> nsresult {
  debug_assert_mut!(url);
  let username = try_or_malformed!(str::from_utf8(username));
  try_or_malformed!(quirks::set_username(url, username));
  NS_OK
}

#[no_mangle]
pub extern "C" fn mozurl_set_password(url: &mut MozURL, password: &nsACString) -> nsresult {
  debug_assert_mut!(url);
  let password = try_or_malformed!(str::from_utf8(password));
  try_or_malformed!(quirks::set_password(url, password));
  NS_OK
}

#[no_mangle]
pub extern "C" fn mozurl_set_host_port(url: &mut MozURL, hostport: &nsACString) -> nsresult {
  debug_assert_mut!(url);
  let hostport = try_or_malformed!(str::from_utf8(hostport));
  try_or_malformed!(quirks::set_host(url, hostport));
  NS_OK
}

#[no_mangle]
pub extern "C" fn mozurl_set_hostname(url: &mut MozURL, host: &nsACString) -> nsresult {
  debug_assert_mut!(url);
  let host = try_or_malformed!(str::from_utf8(host));
  try_or_malformed!(quirks::set_hostname(url, host));
  NS_OK
}

#[no_mangle]
pub extern "C" fn mozurl_set_port_no(url: &mut MozURL, new_port: i32) -> nsresult {
  debug_assert_mut!(url);
  if url.cannot_be_a_base() {
    return NS_ERROR_MALFORMED_URI;
  }

  let port = match new_port {
    new if new < 0 || u16::max_value() as i32 > new => None,
    new if Some(new as u16) == default_port(url.scheme()) => None,
    new => Some(new as u16),
  };
  try_or_malformed!(url.set_port(port));
  NS_OK
}

#[no_mangle]
pub extern "C" fn mozurl_set_pathname(url: &mut MozURL, path: &nsACString) -> nsresult {
  debug_assert_mut!(url);
  let path = try_or_malformed!(str::from_utf8(path));
  quirks::set_pathname(url, path);
  NS_OK
}

#[no_mangle]
pub extern "C" fn mozurl_set_query(url: &mut MozURL, query: &nsACString) -> nsresult {
  debug_assert_mut!(url);
  let query = try_or_malformed!(str::from_utf8(query));
  quirks::set_search(url, query);
  NS_OK
}

#[no_mangle]
pub extern "C" fn mozurl_set_fragment(url: &mut MozURL, fragment: &nsACString) -> nsresult {
  debug_assert_mut!(url);
  let fragment = try_or_malformed!(str::from_utf8(fragment));
  quirks::set_hash(url, fragment);
  NS_OK
}

#[no_mangle]
pub extern "C" fn mozurl_common_base(url1: &MozURL, url2: &MozURL, result: &mut *const MozURL) -> nsresult {
  *result = ptr::null();
  if url1.url == url2.url {
    RefPtr::new(url1).forget(result);
    return NS_OK;
  }

  if url1.scheme() != url2.scheme() ||
     url1.host() != url2.host() ||
     url1.username() != url2.username() ||
     url1.password() != url2.password() ||
     url1.port() != url2.port() {
    return NS_OK;
  }

  match (url1.path_segments(), url2.path_segments()) {
    (Some(path1), Some(path2)) => {
      // Take the shared prefix of path segments
      let mut url = url1.url.clone();
      if let Ok(mut segs) = url.path_segments_mut() {
        segs.clear();
        segs.extend(path1.zip(path2)
                         .take_while(|&(a, b)| a == b)
                         .map(|p| p.0));
      } else {
        return NS_OK;
      }

      MozURL::from_url(url).forget(result);
      NS_OK
    }
    _ => NS_OK,
  }
}

#[no_mangle]
pub extern "C" fn mozurl_relative(url1: &MozURL, url2: &MozURL, result: &mut nsACString) -> nsresult {
  if url1.url == url2.url {
    result.truncate();
    return NS_OK;
  }

  if url1.scheme() != url2.scheme() ||
     url1.host() != url2.host() ||
     url1.username() != url2.username() ||
     url1.password() != url2.password() ||
     url1.port() != url2.port() {
    result.assign(url2.as_ref());
    return NS_OK;
  }

  match (url1.path_segments(), url2.path_segments()) {
    (Some(mut path1), Some(mut path2)) => {
      // Exhaust the part of the iterators that match
      while let (Some(ref p1), Some(ref p2)) = (path1.next(), path2.next()) {
        if p1 != p2 {
          break;
        }
      }

      result.truncate();
      for _ in path1 {
        result.append("../");
      }
      for p2 in path2 {
        result.append(p2);
        result.append("/");
      }
    }
    _ => {
      result.assign(url2.as_ref());
    }
  }
  NS_OK
}

/// This type is used by nsStandardURL
#[no_mangle]
pub extern "C" fn rusturl_parse_ipv6addr(input: &nsACString, addr: &mut nsACString) -> nsresult {
  let ip6 = try_or_malformed!(str::from_utf8(input));
  let host = try_or_malformed!(url::Host::parse(ip6));
  let _ = write!(addr, "{}", host);
  NS_OK
}
