/* 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/. */
mod action;
mod dispatch_callback;
mod error;
mod monitor;
mod request;
mod string;
mod task;
mod xpcom_methods;

pub use self::error::BitsTaskError;
use self::{
    action::Action,
    error::{
        ErrorStage::Pretask,
        ErrorType::{
            FailedToConstructTaskRunnable, FailedToDispatchRunnable, FailedToStartThread,
            InvalidArgument, NotInitialized,
        },
    },
    request::BitsRequest,
    string::Guid_from_nsCString,
    task::{ClientInitData, MonitorDownloadTask, StartDownloadTask},
};
use nsIBits_method; // From xpcom_method.rs

use bits_client::BitsProxyUsage;
use log::{info, warn};
use moz_task::{create_thread, Task, TaskRunnable};
use nserror::{nsresult, NS_ERROR_ALREADY_INITIALIZED, NS_OK};
use nsstring::{nsACString, nsCString};
use std::cell::Cell;
use xpcom::{
    interfaces::{
        nsIBits, nsIBitsNewRequestCallback, nsIRequestObserver, nsISupports, nsIThread,
        nsProxyUsage,
    },
    xpcom, xpcom_method, RefPtr,
};

#[no_mangle]
pub unsafe extern "C" fn new_bits_service(result: *mut *const nsIBits) {
    let service: RefPtr<BitsService> = BitsService::new();
    RefPtr::new(service.coerce::<nsIBits>()).forget(&mut *result);
}

#[xpcom(implement(nsIBits), nonatomic)]
pub struct BitsService {
    // This command thread will be used to send commands (ex: Suspend, Resume)
    // to a running job. It will be started up when the first job is created and
    // shutdown when all jobs have been completed or cancelled.
    command_thread: Cell<Option<RefPtr<nsIThread>>>,
    client_init_data: Cell<Option<ClientInitData>>,
    // This count will track the number of in-progress requests so that the
    // service knows when the command_thread is no longer being used and can be
    // shut down.
    // `BitsRequest::new()` will increment this and it will be decremented
    // either when cancel/complete is called, or when the request is dropped
    // (if it didn't decrement it already).
    // The count will also be incremented when an action to create a request
    // starts and decremented when the action ends and returns the result via
    // the callback. This prevents the command thread from being shut down while
    // a job is being created.
    request_count: Cell<u32>,
}

/// This implements the nsIBits interface, documented in nsIBits.idl, to enable
/// BITS job management. Specifically, this interface can start a download or
/// connect to an existing download. Doing so will create a BitsRequest through
/// which the transfer can be further manipulated.
///
/// This is a primarily asynchronous interface, which is accomplished via
/// callbacks of type nsIBitsNewRequestCallback. The callback is passed in as
/// an argument and is then passed off-thread via a Task. The Task interacts
/// with BITS and is dispatched back to the main thread with the BITS result.
/// Back on the main thread, it returns that result via the callback including,
/// if successful, a BitsRequest.
impl BitsService {
    pub fn new() -> RefPtr<BitsService> {
        BitsService::allocate(InitBitsService {
            command_thread: Cell::new(None),
            client_init_data: Cell::new(None),
            request_count: Cell::new(0),
        })
    }

    fn get_client_init(&self) -> Option<ClientInitData> {
        let maybe_init_data = self.client_init_data.take();
        self.client_init_data.set(maybe_init_data.clone());
        maybe_init_data
    }

    // Returns the handle to the command thread. If it has not been started yet,
    // the thread will be started.
    fn get_command_thread(&self) -> Result<RefPtr<nsIThread>, nsresult> {
        let mut command_thread = self.command_thread.take();
        if command_thread.is_none() {
            command_thread.replace(create_thread("BitsCommander")?);
        }
        self.command_thread.set(command_thread.clone());
        Ok(command_thread.unwrap())
    }

    // Asynchronously shuts down the command thread. The thread is not shutdown
    // until the event queue is empty, so any tasks that were dispatched before
    // this is called will still run.
    // Leaves None in self.command_thread
    fn shutdown_command_thread(&self) {
        if let Some(command_thread) = self.command_thread.take() {
            if let Err(rv) = unsafe { command_thread.AsyncShutdown() }.to_result() {
                warn!("Failed to shut down command thread: {}", rv);
                warn!("Releasing reference to thread that failed to shut down!");
            }
        }
    }

    fn dispatch_runnable_to_command_thread(
        &self,
        task: Box<dyn Task + Send + Sync>,
        task_runnable_name: &'static str,
        action: Action,
    ) -> Result<(), BitsTaskError> {
        let command_thread = self
            .get_command_thread()
            .map_err(|rv| BitsTaskError::from_nsresult(FailedToStartThread, action, Pretask, rv))?;
        let runnable = TaskRunnable::new(task_runnable_name, task).map_err(|rv| {
            BitsTaskError::from_nsresult(FailedToConstructTaskRunnable, action, Pretask, rv)
        })?;
        TaskRunnable::dispatch(runnable, &command_thread).map_err(|rv| {
            BitsTaskError::from_nsresult(FailedToDispatchRunnable, action, Pretask, rv)
        })
    }

    fn inc_request_count(&self) {
        self.request_count.set(self.request_count.get() + 1);
    }

    fn dec_request_count(&self) {
        let mut count = self.request_count.get();
        if count == 0 {
            warn!("Attempted to decrement request count, but it is 0");
            return;
        }
        count -= 1;
        self.request_count.set(count);

        if count == 0 {
            self.shutdown_command_thread();
        }
    }

    xpcom_method!(
        get_initialized => GetInitialized() -> bool
    );
    fn get_initialized(&self) -> Result<bool, nsresult> {
        Ok(self.get_client_init().is_some())
    }

    xpcom_method!(
        init => Init(
            job_name: *const nsACString,
            save_path_prefix: *const nsACString,
            monitor_timeout_ms: u32
        )
    );
    fn init(
        &self,
        job_name: &nsACString,
        save_path_prefix: &nsACString,
        monitor_timeout_ms: u32,
    ) -> Result<(), nsresult> {
        let previous_data = self.client_init_data.take();
        if previous_data.is_some() {
            self.client_init_data.set(previous_data);
            return Err(NS_ERROR_ALREADY_INITIALIZED);
        }

        info!(
            "BitsService initialized with job_name: {}, save_path_prefix: {}, timeout: {}",
            job_name, save_path_prefix, monitor_timeout_ms,
        );

        self.client_init_data.set(Some(ClientInitData::new(
            nsCString::from(job_name),
            nsCString::from(save_path_prefix),
            monitor_timeout_ms,
        )));

        Ok(())
    }

    nsIBits_method!(
        [Action::StartDownload]
        start_download => StartDownload(
            download_url: *const nsACString,
            save_rel_path: *const nsACString,
            proxy: nsProxyUsage,
            no_progress_timeout_secs: u32,
            update_interval_ms: u32,
            observer: *const nsIRequestObserver,
            [optional] context: *const nsISupports,
        )
    );
    fn start_download(
        &self,
        download_url: &nsACString,
        save_rel_path: &nsACString,
        proxy: nsProxyUsage,
        no_progress_timeout_secs: u32,
        update_interval_ms: u32,
        observer: &nsIRequestObserver,
        context: Option<&nsISupports>,
        callback: &nsIBitsNewRequestCallback,
    ) -> Result<(), BitsTaskError> {
        let client_init_data = self
            .get_client_init()
            .ok_or_else(|| BitsTaskError::new(NotInitialized, Action::StartDownload, Pretask))?;
        if update_interval_ms >= client_init_data.monitor_timeout_ms {
            return Err(BitsTaskError::new(
                InvalidArgument,
                Action::StartDownload,
                Pretask,
            ));
        }
        let proxy = match proxy {
            nsIBits::PROXY_NONE => BitsProxyUsage::NoProxy,
            nsIBits::PROXY_PRECONFIG => BitsProxyUsage::Preconfig,
            nsIBits::PROXY_AUTODETECT => BitsProxyUsage::AutoDetect,
            _ => {
                return Err(BitsTaskError::new(
                    InvalidArgument,
                    Action::StartDownload,
                    Pretask,
                ));
            }
        };

        let task: Box<StartDownloadTask> = Box::new(StartDownloadTask::new(
            client_init_data,
            nsCString::from(download_url),
            nsCString::from(save_rel_path),
            proxy,
            no_progress_timeout_secs,
            update_interval_ms,
            RefPtr::new(self),
            RefPtr::new(observer),
            context.map(RefPtr::new),
            RefPtr::new(callback),
        ));

        let dispatch_result = self.dispatch_runnable_to_command_thread(
            task,
            "BitsService::start_download",
            Action::StartDownload,
        );

        if dispatch_result.is_ok() {
            // Increment the request count when we dispatch an action to start
            // a job, decrement it when the action completes. See the
            // declaration of InitBitsService::request_count for details.
            self.inc_request_count();
        }

        dispatch_result
    }

    nsIBits_method!(
        [Action::MonitorDownload]
        monitor_download => MonitorDownload(
            id: *const nsACString,
            update_interval_ms: u32,
            observer: *const nsIRequestObserver,
            [optional] context: *const nsISupports,
        )
    );
    fn monitor_download(
        &self,
        id: &nsACString,
        update_interval_ms: u32,
        observer: &nsIRequestObserver,
        context: Option<&nsISupports>,
        callback: &nsIBitsNewRequestCallback,
    ) -> Result<(), BitsTaskError> {
        let client_init_data = self
            .get_client_init()
            .ok_or_else(|| BitsTaskError::new(NotInitialized, Action::MonitorDownload, Pretask))?;
        if update_interval_ms >= client_init_data.monitor_timeout_ms {
            return Err(BitsTaskError::new(
                InvalidArgument,
                Action::MonitorDownload,
                Pretask,
            ));
        }
        let guid = Guid_from_nsCString(&nsCString::from(id), Action::MonitorDownload, Pretask)?;

        let task: Box<MonitorDownloadTask> = Box::new(MonitorDownloadTask::new(
            client_init_data,
            guid,
            update_interval_ms,
            RefPtr::new(self),
            RefPtr::new(observer),
            context.map(RefPtr::new),
            RefPtr::new(callback),
        ));

        let dispatch_result = self.dispatch_runnable_to_command_thread(
            task,
            "BitsService::monitor_download",
            Action::MonitorDownload,
        );

        if dispatch_result.is_ok() {
            // Increment the request count when we dispatch an action to start
            // a job, decrement it when the action completes. See the
            // declaration of InitBitsService::request_count for details.
            self.inc_request_count();
        }

        dispatch_result
    }
}

impl Drop for BitsService {
    fn drop(&mut self) {
        self.shutdown_command_thread();
    }
}
