use ribir_types::{Point, Size, Transform};
use smallvec::SmallVec;
use widget_id::RenderQueryable;

use crate::prelude::*;

/// This trait is for a render widget that does not need to be an independent
/// node in the widget tree. It can serve as a wrapper for another render
/// widget.
///
/// # Which widgets should implement this trait?
///
/// If a render widget accepts a single child and its layout size matches its
/// child size, it can be implemented as a `WrapRender` instead of `Render`,
/// eliminating the need to allocate a node in the widget tree.
pub trait WrapRender {
  fn measure(&self, clamp: BoxClamp, host: &dyn Render, ctx: &mut MeasureCtx) -> Size {
    host.measure(clamp, ctx)
  }

  fn place_children(&self, size: Size, host: &dyn Render, ctx: &mut PlaceCtx) {
    host.place_children(size, ctx)
  }

  fn paint(&self, host: &dyn Render, ctx: &mut PaintingCtx) { host.paint(ctx) }

  fn size_affected_by_child(&self, host: &dyn Render) -> bool {
    // Detected by its host by default, so we return true here.
    host.size_affected_by_child()
  }

  fn hit_test(&self, host: &dyn Render, ctx: &mut HitTestCtx, pos: Point) -> HitTest {
    host.hit_test(ctx, pos)
  }

  fn get_transform(&self, host: &dyn Render) -> Option<Transform> { host.get_transform() }

  fn dirty_phase(&self, host: &dyn Render) -> DirtyPhase { host.dirty_phase() }

  fn adjust_position(&self, host: &dyn Render, pos: Point, ctx: &mut PlaceCtx) -> Point {
    host.adjust_position(pos, ctx)
  }

  #[cfg(feature = "debug")]
  fn debug_type(&self) -> Option<&'static str> { None }

  #[cfg(feature = "debug")]
  fn debug_properties(&self) -> Option<serde_json::Value> { None }

  fn wrapper_dirty_phase(&self) -> DirtyPhase;

  fn combine_x_multi_child(this: impl StateWriter<Value = Self>, x: XMultiChild) -> XMultiChild
  where
    Self: Sized + 'static,
  {
    let combine = combine_method::<Self>(this);
    let parent = CombinedParent { combine: Box::new(combine), parent: x.0 };
    XChild::from_boxed(Box::new(parent))
  }

  fn combine_x_single_child(this: impl StateWriter<Value = Self>, x: XSingleChild) -> XSingleChild
  where
    Self: Sized + 'static,
  {
    let combine = combine_method::<Self>(this);
    let parent = CombinedParent { combine: Box::new(combine), parent: x.0 };
    XChild::from_boxed(Box::new(parent))
  }

  fn combine_child(this: impl StateWriter<Value = Self>, child: Widget) -> Widget
  where
    Self: Sized + 'static,
  {
    let combine = combine_method::<Self>(this);
    combine(child)
  }
}

struct RenderPair {
  wrapper: Box<dyn WrapRender>,
  host: Box<dyn RenderQueryable>,
}

impl Query for RenderPair {
  fn query_all<'q>(&'q self, query_id: &QueryId, out: &mut SmallVec<[QueryHandle<'q>; 1]>) {
    self.host.query_all(query_id, out)
  }

  fn query_all_write<'q>(&'q self, query_id: &QueryId, out: &mut SmallVec<[QueryHandle<'q>; 1]>) {
    self.host.query_all_write(query_id, out)
  }

  fn query<'q>(&'q self, query_id: &QueryId) -> Option<QueryHandle<'q>> {
    self.host.query(query_id)
  }

  fn query_write<'q>(&'q self, query_id: &QueryId) -> Option<QueryHandle<'q>> {
    self.host.query_write(query_id)
  }

  fn queryable(&self) -> bool { self.host.queryable() }
}

impl Render for RenderPair {
  fn measure(&self, clamp: BoxClamp, ctx: &mut MeasureCtx) -> Size {
    self.wrapper.measure(clamp, &*self.host, ctx)
  }

  fn place_children(&self, size: Size, ctx: &mut PlaceCtx) {
    self
      .wrapper
      .place_children(size, &*self.host, ctx)
  }

  fn paint(&self, ctx: &mut PaintingCtx) { self.wrapper.paint(&*self.host, ctx); }

  fn size_affected_by_child(&self) -> bool { self.wrapper.size_affected_by_child(&*self.host) }

  fn hit_test(&self, ctx: &mut HitTestCtx, pos: Point) -> HitTest {
    self
      .wrapper
      .hit_test(self.host.as_render(), ctx, pos)
  }

  fn dirty_phase(&self) -> DirtyPhase { self.wrapper.dirty_phase(self.host.as_render()) }

  fn get_transform(&self) -> Option<Transform> { self.wrapper.get_transform(self.host.as_render()) }

  fn adjust_position(&self, pos: Point, ctx: &mut PlaceCtx) -> Point {
    self
      .wrapper
      .adjust_position(self.host.as_render(), pos, ctx)
  }

  #[cfg(feature = "debug")]
  fn debug_name(&self) -> std::borrow::Cow<'static, str> { self.host.as_render().debug_name() }

  #[cfg(feature = "debug")]
  fn debug_properties(&self) -> serde_json::Value {
    use serde_json::{Map, Value, json};

    let host_props = self.host.as_render().debug_properties();
    let mut obj: Map<String, Value> = match host_props {
      Value::Object(map) => map,
      Value::Null => Map::new(),
      other => {
        let mut map = Map::new();
        map.insert("_value".to_string(), other);
        map
      }
    };

    if let Some(ty) = self.wrapper.debug_type() {
      let wrapper_props = self
        .wrapper
        .debug_properties()
        .unwrap_or(Value::Null);
      let entry = json!({ "type": ty, "properties": wrapper_props });

      match obj.get_mut("_wrappers") {
        Some(Value::Array(arr)) => arr.insert(0, entry),
        Some(other) => {
          let prev = std::mem::replace(other, Value::Array(vec![entry]));
          if !prev.is_null()
            && let Value::Array(arr) = other
          {
            arr.push(prev);
          }
        }
        None => {
          obj.insert("_wrappers".to_string(), Value::Array(vec![entry]));
        }
      }
    }

    Value::Object(obj)
  }
}

impl<R> WrapRender for R
where
  R: StateReader,
  R::Value: WrapRender,
{
  fn measure(&self, clamp: BoxClamp, host: &dyn Render, ctx: &mut MeasureCtx) -> Size {
    self.read().measure(clamp, host, ctx)
  }

  fn place_children(&self, size: Size, host: &dyn Render, ctx: &mut PlaceCtx) {
    self.read().place_children(size, host, ctx)
  }

  fn paint(&self, host: &dyn Render, ctx: &mut PaintingCtx) { self.read().paint(host, ctx) }

  fn size_affected_by_child(&self, host: &dyn Render) -> bool {
    self.read().size_affected_by_child(host)
  }

  fn hit_test(&self, host: &dyn Render, ctx: &mut HitTestCtx, pos: Point) -> HitTest {
    self.read().hit_test(host, ctx, pos)
  }

  fn get_transform(&self, host: &dyn Render) -> Option<Transform> {
    self.read().get_transform(host)
  }

  /// Returns the dirty phase of the wrapped render, this value should
  /// always be the same.
  fn wrapper_dirty_phase(&self) -> DirtyPhase { self.read().wrapper_dirty_phase() }

  fn adjust_position(&self, host: &dyn Render, pos: Point, ctx: &mut PlaceCtx) -> Point {
    self.read().adjust_position(host, pos, ctx)
  }

  #[cfg(feature = "debug")]
  fn debug_type(&self) -> Option<&'static str> { self.read().debug_type() }

  #[cfg(feature = "debug")]
  fn debug_properties(&self) -> Option<serde_json::Value> { self.read().debug_properties() }
}

#[macro_export]
macro_rules! impl_compose_child_for_wrap_render {
  ($name:ty) => {
    impl<'c> ComposeChild<'c> for $name {
      type Child = Widget<'c>;
      fn compose_child(this: impl StateWriter<Value = Self>, child: Self::Child) -> Widget<'c> {
        WrapRender::combine_child(this, child)
      }
    }
  };
}

pub(crate) use impl_compose_child_for_wrap_render;

pub struct CombinedParent<'p> {
  combine: Box<dyn FnOnce(Widget) -> Widget>,
  parent: Box<dyn BoxedParent + 'p>,
}

impl<'p> BoxedParent for CombinedParent<'p> {
  fn boxed_with_children<'w>(self: Box<Self>, children: Vec<Widget<'w>>) -> Widget<'w>
  where
    Self: 'w,
  {
    let Self { combine, parent } = *self;
    let widget = parent.boxed_with_children(children);
    combine(widget)
  }
}

fn combine_method<Wrapper: WrapRender + 'static>(
  this: impl StateWriter<Value = Wrapper>,
) -> impl FnOnce(Widget) -> Widget {
  let dirty_phase = this.wrapper_dirty_phase();
  move |mut host| {
    let wrapper: Box<dyn WrapRender> = match this.try_into_value() {
      Ok(this) => Box::new(this),
      Err(this) => {
        let reader = match this.into_reader() {
          Ok(r) => r,
          Err(s) => {
            host = host.dirty_on(s.raw_modifies(), dirty_phase);
            s.clone_reader()
          }
        };
        Box::new(reader)
      }
    };

    host.on_build(|id| {
      id.wrap_node(BuildCtx::get_mut().tree_mut(), move |host| {
        Box::new(RenderPair { wrapper, host })
      });
    })
  }
}
