/* 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/. */
use dom::attr::{Attr, AttrHelpersForLayout, AttrValue};
use dom::bindings::codegen::Bindings::HTMLIFrameElementBinding;
use dom::bindings::codegen::Bindings::HTMLIFrameElementBinding::HTMLIFrameElementMethods;
use dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
use dom::bindings::codegen::InheritTypes::{ElementCast, EventCast, EventTargetCast};
use dom::bindings::codegen::InheritTypes::{HTMLElementCast, NodeCast};
use dom::bindings::conversions::ToJSValConvertible;
use dom::bindings::error::{Error, ErrorResult, Fallible};
use dom::bindings::global::GlobalRef;
use dom::bindings::js::{Root, LayoutJS};
use dom::bindings::utils::Reflectable;
use dom::customevent::CustomEvent;
use dom::document::Document;
use dom::element::{self, AttributeMutation};
use dom::htmlelement::HTMLElement;
use dom::node::{Node, window_from_node};
use dom::urlhelper::UrlHelper;
use dom::virtualmethods::VirtualMethods;
use dom::window::Window;
use js::jsapi::{JSAutoCompartment, JSAutoRequest, RootedValue};
use js::jsval::UndefinedValue;
use msg::constellation_msg::IFrameSandboxState::{IFrameSandboxed, IFrameUnsandboxed};
use msg::constellation_msg::Msg as ConstellationMsg;
use msg::constellation_msg::{ConstellationChan, IframeLoadInfo, MozBrowserEvent};
use msg::constellation_msg::{NavigationDirection, PipelineId, SubpageId};
use page::IterablePage;
use std::ascii::AsciiExt;
use std::borrow::ToOwned;
use std::cell::Cell;
use string_cache::Atom;
use url::{Url, UrlParser};
use util::prefs;
use util::str::DOMString;
use util::str::{self, LengthOrPercentageOrAuto};
pub fn mozbrowser_enabled() -> bool {
    prefs::get_pref("dom.mozbrowser.enabled").as_boolean().unwrap_or(false)
}
#[derive(HeapSizeOf)]
enum SandboxAllowance {
    AllowNothing = 0x00,
    AllowSameOrigin = 0x01,
    AllowTopNavigation = 0x02,
    AllowForms = 0x04,
    AllowScripts = 0x08,
    AllowPointerLock = 0x10,
    AllowPopups = 0x20
}
#[dom_struct]
pub struct HTMLIFrameElement {
    htmlelement: HTMLElement,
    pipeline_id: Cell>,
    subpage_id: Cell >,
    containing_page_pipeline_id: Cell >,
    sandbox: Cell >,
}
impl HTMLIFrameElement {
    pub fn is_sandboxed(&self) -> bool {
        self.sandbox.get().is_some()
    }
    pub fn get_url(&self) -> Option {
        let element = ElementCast::from_ref(self);
        element.get_attribute(&ns!(""), &atom!("src")).and_then(|src| {
            let url = src.r().value();
            if url.is_empty() {
                None
            } else {
                let window = window_from_node(self);
                UrlParser::new().base_url(&window.r().get_url())
                    .parse(&url).ok()
            }
        })
    }
    pub fn generate_new_subpage_id(&self) -> (SubpageId, Option) {
        self.pipeline_id.set(Some(PipelineId::new()));
        let old_subpage_id = self.subpage_id.get();
        let win = window_from_node(self);
        let subpage_id = win.r().get_next_subpage_id();
        self.subpage_id.set(Some(subpage_id));
        (subpage_id, old_subpage_id)
    }
    pub fn navigate_child_browsing_context(&self, url: Url) {
        let sandboxed = if self.is_sandboxed() {
            IFrameSandboxed
        } else {
            IFrameUnsandboxed
        };
        let window = window_from_node(self);
        let window = window.r();
        let (new_subpage_id, old_subpage_id) = self.generate_new_subpage_id();
        let new_pipeline_id = self.pipeline_id.get().unwrap();
        self.containing_page_pipeline_id.set(Some(window.pipeline()));
        let ConstellationChan(ref chan) = window.constellation_chan();
        let load_info = IframeLoadInfo {
            url: url,
            containing_pipeline_id: window.pipeline(),
            new_subpage_id: new_subpage_id,
            old_subpage_id: old_subpage_id,
            new_pipeline_id: new_pipeline_id,
            sandbox: sandboxed,
        };
        chan.send(ConstellationMsg::ScriptLoadedURLInIFrame(load_info)).unwrap();
        if mozbrowser_enabled() {
            // https://developer.mozilla.org/en-US/docs/Web/Events/mozbrowserloadstart
            self.dispatch_mozbrowser_event(MozBrowserEvent::LoadStart);
        }
    }
    pub fn process_the_iframe_attributes(&self) {
        let url = match self.get_url() {
            Some(url) => url.clone(),
            None => Url::parse("about:blank").unwrap(),
        };
        self.navigate_child_browsing_context(url);
    }
    pub fn dispatch_mozbrowser_event(&self, event: MozBrowserEvent) {
        // TODO(gw): Support mozbrowser event types that have detail which is not a string.
        // See https://developer.mozilla.org/en-US/docs/Web/API/Using_the_Browser_API
        // for a list of mozbrowser events.
        assert!(mozbrowser_enabled());
        if self.Mozbrowser() {
            let window = window_from_node(self);
            let cx = window.r().get_cx();
            let _ar = JSAutoRequest::new(cx);
            let _ac = JSAutoCompartment::new(cx, window.reflector().get_jsobject().get());
            let mut detail = RootedValue::new(cx, UndefinedValue());
            event.detail().to_jsval(cx, detail.handle_mut());
            let custom_event = CustomEvent::new(GlobalRef::Window(window.r()),
                                                event.name().to_owned(),
                                                true,
                                                true,
                                                detail.handle());
            let target = EventTargetCast::from_ref(self);
            let event = EventCast::from_ref(custom_event.r());
            event.fire(target);
        }
    }
    pub fn update_subpage_id(&self, new_subpage_id: SubpageId) {
        self.subpage_id.set(Some(new_subpage_id));
    }
    #[allow(unsafe_code)]
    pub fn get_width(&self) -> LengthOrPercentageOrAuto {
        unsafe {
            element::get_attr_for_layout(ElementCast::from_ref(&*self),
                                         &ns!(""),
                                         &atom!("width")).map(|attribute| {
                str::parse_length(&**attribute.value_for_layout())
            }).unwrap_or(LengthOrPercentageOrAuto::Auto)
        }
    }
    #[allow(unsafe_code)]
    pub fn get_height(&self) -> LengthOrPercentageOrAuto {
        unsafe {
            element::get_attr_for_layout(ElementCast::from_ref(&*self),
                                         &ns!(""),
                                         &atom!("height")).map(|attribute| {
                str::parse_length(&**attribute.value_for_layout())
            }).unwrap_or(LengthOrPercentageOrAuto::Auto)
        }
    }
    fn new_inherited(localName: DOMString,
                     prefix: Option,
                     document: &Document) -> HTMLIFrameElement {
        HTMLIFrameElement {
            htmlelement: HTMLElement::new_inherited(localName, prefix, document),
            pipeline_id: Cell::new(None),
            subpage_id: Cell::new(None),
            containing_page_pipeline_id: Cell::new(None),
            sandbox: Cell::new(None),
        }
    }
    #[allow(unrooted_must_root)]
    pub fn new(localName: DOMString,
               prefix: Option,
               document: &Document) -> Root {
        let element = HTMLIFrameElement::new_inherited(localName, prefix, document);
        Node::reflect_node(box element, document, HTMLIFrameElementBinding::Wrap)
    }
    #[inline]
    pub fn containing_page_pipeline_id(&self) -> Option {
        self.containing_page_pipeline_id.get()
    }
    #[inline]
    pub fn subpage_id(&self) -> Option {
        self.subpage_id.get()
    }
}
pub trait HTMLIFrameElementLayoutMethods {
    fn pipeline_id(self) -> Option;
}
impl HTMLIFrameElementLayoutMethods for LayoutJS {
    #[inline]
    #[allow(unsafe_code)]
    fn pipeline_id(self) -> Option {
        unsafe {
            (*self.unsafe_get()).pipeline_id.get()
        }
    }
}
pub fn Navigate(iframe: &HTMLIFrameElement, direction: NavigationDirection) -> Fallible<()> {
    if iframe.Mozbrowser() {
        let node = NodeCast::from_ref(iframe);
        if node.is_in_doc() {
            let window = window_from_node(iframe);
            let window = window.r();
            let pipeline_info = Some((iframe.containing_page_pipeline_id().unwrap(),
                                      iframe.subpage_id().unwrap()));
            let ConstellationChan(ref chan) = window.constellation_chan();
            let msg = ConstellationMsg::Navigate(pipeline_info, direction);
            chan.send(msg).unwrap();
        }
        Ok(())
    } else {
        debug!("this frame is not mozbrowser (or experimental_enabled is false)");
        Err(Error::NotSupported)
    }
}
impl HTMLIFrameElementMethods for HTMLIFrameElement {
    // https://html.spec.whatwg.org/multipage/#dom-iframe-src
    fn Src(&self) -> DOMString {
        let element = ElementCast::from_ref(self);
        element.get_string_attribute(&atom!("src"))
    }
    // https://html.spec.whatwg.org/multipage/#dom-iframe-src
    fn SetSrc(&self, src: DOMString) {
        let element = ElementCast::from_ref(self);
        element.set_url_attribute(&atom!("src"), src)
    }
    // https://html.spec.whatwg.org/multipage/#dom-iframe-sandbox
    fn Sandbox(&self) -> DOMString {
        let element = ElementCast::from_ref(self);
        element.get_string_attribute(&atom!("sandbox"))
    }
    // https://html.spec.whatwg.org/multipage/#dom-iframe-sandbox
    fn SetSandbox(&self, sandbox: DOMString) {
        let element = ElementCast::from_ref(self);
        element.set_tokenlist_attribute(&atom!("sandbox"), sandbox);
    }
    // https://html.spec.whatwg.org/multipage/#dom-iframe-contentwindow
    fn GetContentWindow(&self) -> Option> {
        self.subpage_id.get().and_then(|subpage_id| {
            let window = window_from_node(self);
            let window = window.r();
            let children = window.page().children.borrow();
            children.iter().find(|page| {
                let window = page.window();
                window.r().subpage() == Some(subpage_id)
            }).map(|page| page.window())
        })
    }
    // https://html.spec.whatwg.org/multipage/#dom-iframe-contentdocument
    fn GetContentDocument(&self) -> Option> {
        self.GetContentWindow().and_then(|window| {
            let self_url = match self.get_url() {
                Some(self_url) => self_url,
                None => return None,
            };
            let win_url = window_from_node(self).r().get_url();
            if UrlHelper::SameOrigin(&self_url, &win_url) {
                Some(window.r().Document())
            } else {
                None
            }
        })
    }
    // Experimental mozbrowser implementation is based on the webidl
    // present in the gecko source tree, and the documentation here:
    // https://developer.mozilla.org/en-US/docs/Web/API/Using_the_Browser_API
    // TODO(gw): Use experimental codegen when it is available to avoid
    // exposing these APIs. See https://github.com/servo/servo/issues/5264.
    // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-mozbrowser
    fn Mozbrowser(&self) -> bool {
        if mozbrowser_enabled() {
            let element = ElementCast::from_ref(self);
            element.has_attribute(&Atom::from_slice("mozbrowser"))
        } else {
            false
        }
    }
    // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-mozbrowser
    fn SetMozbrowser(&self, value: bool) -> ErrorResult {
        if mozbrowser_enabled() {
            let element = ElementCast::from_ref(self);
            element.set_bool_attribute(&Atom::from_slice("mozbrowser"), value);
        }
        Ok(())
    }
    // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/goBack
    fn GoBack(&self) -> Fallible<()> {
        Navigate(self, NavigationDirection::Back)
    }
    // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/goForward
    fn GoForward(&self) -> Fallible<()> {
        Navigate(self, NavigationDirection::Forward)
    }
    // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/reload
    fn Reload(&self, _hardReload: bool) -> Fallible<()> {
        Err(Error::NotSupported)
    }
    // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/stop
    fn Stop(&self) -> Fallible<()> {
        Err(Error::NotSupported)
    }
    // https://html.spec.whatwg.org/multipage/#dom-dim-width
    make_getter!(Width);
    // https://html.spec.whatwg.org/multipage/#dom-dim-width
    make_setter!(SetWidth, "width");
    // https://html.spec.whatwg.org/multipage/#dom-dim-height
    make_getter!(Height);
    // https://html.spec.whatwg.org/multipage/#dom-dim-height
    make_setter!(SetHeight, "height");
}
impl VirtualMethods for HTMLIFrameElement {
    fn super_type<'b>(&'b self) -> Option<&'b VirtualMethods> {
        let htmlelement: &HTMLElement = HTMLElementCast::from_ref(self);
        Some(htmlelement as &VirtualMethods)
    }
    fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation) {
        self.super_type().unwrap().attribute_mutated(attr, mutation);
        match attr.local_name() {
            &atom!(sandbox) => {
                self.sandbox.set(mutation.new_value(attr).map(|value| {
                    let mut modes = SandboxAllowance::AllowNothing as u8;
                    for token in value.as_tokens() {
                        modes |= match &*token.to_ascii_lowercase() {
                            "allow-same-origin" => SandboxAllowance::AllowSameOrigin,
                            "allow-forms" => SandboxAllowance::AllowForms,
                            "allow-pointer-lock" => SandboxAllowance::AllowPointerLock,
                            "allow-popups" => SandboxAllowance::AllowPopups,
                            "allow-scripts" => SandboxAllowance::AllowScripts,
                            "allow-top-navigation" => SandboxAllowance::AllowTopNavigation,
                            _ => SandboxAllowance::AllowNothing
                        } as u8;
                    }
                    modes
                }));
            },
            &atom!(src) => {
                if let AttributeMutation::Set(_) = mutation {
                    if NodeCast::from_ref(self).is_in_doc() {
                        self.process_the_iframe_attributes();
                    }
                }
            },
            _ => {},
        }
    }
    fn parse_plain_attribute(&self, name: &Atom, value: DOMString) -> AttrValue {
        match name {
            &atom!("sandbox") => AttrValue::from_serialized_tokenlist(value),
            _ => self.super_type().unwrap().parse_plain_attribute(name, value),
        }
    }
    fn bind_to_tree(&self, tree_in_doc: bool) {
        if let Some(ref s) = self.super_type() {
            s.bind_to_tree(tree_in_doc);
        }
        if tree_in_doc {
            self.process_the_iframe_attributes();
        }
    }
    fn unbind_from_tree(&self, tree_in_doc: bool) {
        if let Some(ref s) = self.super_type() {
            s.unbind_from_tree(tree_in_doc);
        }
        // https://html.spec.whatwg.org/multipage/#a-browsing-context-is-discarded
        if let Some(pipeline_id) = self.pipeline_id.get() {
            let window = window_from_node(self);
            let window = window.r();
            let ConstellationChan(ref chan) = window.constellation_chan();
            let msg = ConstellationMsg::RemoveIFrame(pipeline_id);
            chan.send(msg).unwrap();
            // Resetting the subpage id to None is required here so that
            // if this iframe is subsequently re-added to the document
            // the load doesn't think that it's a navigation, but instead
            // a new iframe. Without this, the constellation gets very
            // confused.
            self.subpage_id.set(None);
            self.pipeline_id.set(None);
        }
    }
}