use crate::clipboard_rs::{
  common::Result, Clipboard, ClipboardContent, ClipboardHandler, ClipboardWatcher, ContentFormat,
};
#[cfg(feature = "image")]
use crate::clipboard_rs::{common::RustImage, RustImageData};
use objc2::{rc::Retained, runtime::ProtocolObject};
use objc2_foundation::{ns_string, NSArray, NSData, NSDictionary, NSString};
use objc2_ui_kit::UIPasteboard;
#[cfg(feature = "image")]
use objc2_ui_kit::{UIImage, UIImagePNGRepresentation};
use std::{
  sync::mpsc::{self, Receiver, Sender},
  time::Duration,
};

pub struct ClipboardContext {
  clipboard: Retained<UIPasteboard>,
}
pub struct ClipboardWatcherContext<T: ClipboardHandler> {
  clipboard: Retained<UIPasteboard>,
  handlers: Vec<T>,
  running: bool,
  stop_signal: Sender<()>,
  stop_receiver: Receiver<()>,
}

impl<T: ClipboardHandler> ClipboardWatcherContext<T> {
  pub fn new() -> Result<Self> {
    let clipboard = unsafe { UIPasteboard::generalPasteboard() };
    let (tx, rx) = mpsc::channel();
    Ok(Self {
      clipboard,
      handlers: Vec::new(),
      running: false,
      stop_signal: tx,
      stop_receiver: rx,
    })
  }
}

unsafe impl<T: ClipboardHandler> Send for ClipboardWatcherContext<T> {}

impl<T: ClipboardHandler> ClipboardWatcher<T> for ClipboardWatcherContext<T> {
  fn add_handler(&mut self, handler: T) -> &mut Self {
    self.handlers.push(handler);
    self
  }

  fn start_watch(&mut self) {
    if self.running {
      println!("already start watch!");
      return;
    }
    if self.handlers.is_empty() {
      println!("no handler, no need to start watch!");
      return;
    }
    self.running = true;
    let mut last_change_count = unsafe { self.clipboard.changeCount() };
    loop {
      // if receive stop signal, break loop
      if self
        .stop_receiver
        .recv_timeout(Duration::from_millis(500))
        .is_ok()
      {
        break;
      }
      let change_count = unsafe { self.clipboard.changeCount() };
      if last_change_count == 0 {
        last_change_count = change_count;
      } else if change_count != last_change_count {
        self
          .handlers
          .iter_mut()
          .for_each(|handler| handler.on_clipboard_change());
        last_change_count = change_count;
      }
    }
    self.running = false;
  }

  fn get_shutdown_channel(&self) -> WatcherShutdown {
    WatcherShutdown {
      stop_signal: self.stop_signal.clone(),
    }
  }
}

pub struct WatcherShutdown {
  stop_signal: Sender<()>,
}

impl Drop for WatcherShutdown {
  fn drop(&mut self) {
    let _ = self.stop_signal.send(());
  }
}

impl ClipboardContext {
  pub fn new() -> Result<Self> {
    let clipboard = unsafe { UIPasteboard::generalPasteboard() };
    Ok(Self { clipboard })
  }

  fn write_to_clipboard(&self, data: &[ClipboardContent]) -> Result<()> {
    let items = data
      .iter()
      .map(|content| match content {
        ClipboardContent::Text(text) => {
          let ns_text = NSString::from_str(text);
          let pair = unsafe {
            NSDictionary::dictionaryWithObject_forKey(
              ns_text.as_ref(),
              ProtocolObject::from_ref(ns_string!("public.utf8-plain-text")),
            )
          };
          Some(pair)
        }
        ClipboardContent::Rtf(rtf) => {
          let ns_rtf = NSString::from_str(rtf);
          let pair = unsafe {
            NSDictionary::dictionaryWithObject_forKey(
              ns_rtf.as_ref(),
              ProtocolObject::from_ref(ns_string!("public.rtf")),
            )
          };
          Some(pair)
        }
        ClipboardContent::Html(html) => {
          let ns_html = NSString::from_str(html);
          let pair = unsafe {
            NSDictionary::dictionaryWithObject_forKey(
              ns_html.as_ref(),
              ProtocolObject::from_ref(ns_string!("public.html")),
            )
          };
          Some(pair)
        }
        #[cfg(feature = "image")]
        ClipboardContent::Image(image) => {
          let png = image.to_png().unwrap();
          let ns_data = NSData::with_bytes(png.get_bytes());
          let image = unsafe { UIImage::imageWithData(&ns_data) };
          if let Some(image) = image {
            let pair = unsafe {
              NSDictionary::dictionaryWithObject_forKey(
                image.as_ref(),
                ProtocolObject::from_ref(ns_string!("public.png")),
              )
            };
            Some(pair)
          } else {
            None
          }
        }
        _ => None,
      })
      .filter_map(|item| item)
      .collect::<Vec<_>>();
    unsafe {
      self
        .clipboard
        .setItems(&NSArray::from_retained_slice(&items))
    };
    Ok(())
  }
}

impl Clipboard for ClipboardContext {
  fn available_formats(&self) -> Result<Vec<String>> {
    let formats = unsafe { self.clipboard.pasteboardTypes() };
    Ok(formats.iter().map(|f| f.to_string()).collect())
  }

  fn has(&self, format: ContentFormat) -> bool {
    match format {
      ContentFormat::Text => unsafe { self.clipboard.hasStrings() },
      #[cfg(feature = "image")]
      ContentFormat::Image => unsafe { self.clipboard.hasImages() },
      ContentFormat::Rtf => unsafe {
        self
          .clipboard
          .containsPasteboardTypes(&NSArray::from_slice(&[ns_string!("public.rtf")]))
      },
      ContentFormat::Html => unsafe {
        self
          .clipboard
          .containsPasteboardTypes(&NSArray::from_slice(&[ns_string!("public.html")]))
      },
      ContentFormat::Files => false,
      ContentFormat::Other(format) => unsafe {
        self
          .clipboard
          .containsPasteboardTypes(&NSArray::from_retained_slice(&[NSString::from_str(
            &format,
          )]))
      },
    }
  }

  fn clear(&self) -> Result<()> {
    unsafe { self.clipboard.setItems(&NSArray::from_slice(&[])) };
    Ok(())
  }

  fn get_buffer(&self, format: &str) -> Result<Vec<u8>> {
    let ns_format = NSString::from_str(format);
    let data = unsafe { self.clipboard.dataForPasteboardType(&ns_format) };
    if let Some(data) = data {
      Ok(data.to_vec())
    } else {
      Err("No data found".into())
    }
  }

  fn get_text(&self) -> Result<String> {
    let text = unsafe { self.clipboard.string() };
    if let Some(text) = text {
      Ok(text.to_string())
    } else {
      Err("No text found".into())
    }
  }

  fn get_rich_text(&self) -> Result<String> {
    let buffer = self.get_buffer("public.rtf")?;
    Ok(String::from_utf8_lossy(&buffer).to_string())
  }

  fn get_html(&self) -> Result<String> {
    let buffer = self.get_buffer("public.html")?;
    Ok(String::from_utf8_lossy(&buffer).to_string())
  }

  #[cfg(feature = "image")]
  fn get_image(&self) -> Result<RustImageData> {
    let image = unsafe { self.clipboard.image() };
    if let Some(image) = image {
      let data = unsafe { UIImagePNGRepresentation(&image) };
      if let Some(data) = data {
        let bytes = unsafe { data.as_bytes_unchecked() };
        Ok(RustImageData::from_bytes(bytes)?)
      } else {
        Err("No image data found".into())
      }
    } else {
      Err("No image found".into())
    }
  }

  fn get_files(&self) -> Result<Vec<String>> {
    Err("Not supported".into())
  }

  fn get(&self, _formats: &[ContentFormat]) -> Result<Vec<ClipboardContent>> {
    Err("Not supported".into())
  }

  fn set_buffer(&self, format: &str, buffer: Vec<u8>) -> Result<()> {
    let ns_format = NSString::from_str(format);
    let ns_data = NSData::with_bytes(&buffer);
    unsafe {
      self
        .clipboard
        .setData_forPasteboardType(&ns_data, &ns_format)
    }
    Ok(())
  }

  fn set_text(&self, text: String) -> Result<()> {
    unsafe {
      self
        .clipboard
        .setString(Some(&NSString::from_str(text.as_str())))
    };
    Ok(())
  }

  fn set_rich_text(&self, _text: String) -> Result<()> {
    Err("Not supported".into())
  }

  fn set_html(&self, _html: String) -> Result<()> {
    Err("Not supported".into())
  }

  #[cfg(feature = "image")]
  fn set_image(&self, image: RustImageData) -> Result<()> {
    if image.is_empty() {
      Err("Image is empty".into())
    } else {
      let png = image.to_png()?;
      let ns_data = NSData::with_bytes(png.get_bytes());
      let image = unsafe { UIImage::imageWithData(&ns_data) };
      if let Some(image) = image {
        unsafe { self.clipboard.setImage(Some(&image)) };
        Ok(())
      } else {
        Err("Failed to create image".into())
      }
    }
  }

  fn set_files(&self, _files: Vec<String>) -> Result<()> {
    Err("Not supported".into())
  }

  fn set(&self, contents: Vec<ClipboardContent>) -> Result<()> {
    self.write_to_clipboard(&contents)?;
    Ok(())
  }
}
