use toml::Value;

use crate::CFA;
use crate::cfa::PlaneColor;
use crate::imgop::xyz::FlatColorMatrix;
use crate::imgop::xyz::Illuminant;

use std::collections::HashMap;

use super::BlackLevel;
use super::WhiteLevel;

/// Contains sanitized information about the raw image's properties
#[derive(Debug, Clone, Default)]
pub struct Camera {
  pub make: String,
  pub model: String,
  pub mode: String,
  pub clean_make: String,
  pub clean_model: String,
  pub remark: Option<String>,
  pub filesize: usize,
  pub raw_width: usize,
  pub raw_height: usize,
  //pub orientation: Orientation,
  pub whitelevel: Option<Vec<u32>>,
  pub blacklevel: Option<Vec<u32>>,
  pub blackareah: Option<(usize, usize)>,
  pub blackareav: Option<(usize, usize)>,
  pub xyz_to_cam: [[f32; 3]; 4],
  pub color_matrix: HashMap<Illuminant, FlatColorMatrix>,
  pub cfa: CFA,
  pub plane_color: PlaneColor,
  // Active area relative to sensor size
  pub active_area: Option<[usize; 4]>,
  // Recommended area relative to sensor size
  pub crop_area: Option<[usize; 4]>,
  // Hint/Replacement for EXIF BITDEPTH info
  pub bps: Option<usize>,
  // The BPS of the output after decoding
  pub real_bps: usize,
  pub highres_width: usize,
  pub default_scale: DefaultScale,
  pub best_quality_scale: BestQualityScale,
  pub hints: Vec<String>,
  pub params: HashMap<String, Value>,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DefaultScale(pub [[u32; 2]; 2]);

impl Default for DefaultScale {
  fn default() -> Self {
    Self([[1, 1], [1, 1]])
  }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct BestQualityScale(pub [u32; 2]);

impl Default for BestQualityScale {
  fn default() -> Self {
    Self([1, 1])
  }
}

impl Camera {
  pub fn find_hint(&self, hint: &str) -> bool {
    self.hints.contains(&hint.to_string())
  }

  pub fn param_usize(&self, name: &str) -> Option<usize> {
    self.params.get(name).and_then(|p| p.as_integer()).map(|i| i as usize)
  }

  pub fn param_i32(&self, name: &str) -> Option<i32> {
    self.params.get(name).and_then(|p| p.as_integer()).map(|i| i as i32)
  }

  pub fn param_str(&self, name: &str) -> Option<&str> {
    self.params.get(name).and_then(|p| p.as_str())
  }

  pub fn make_blacklevel(&self, cpp: usize) -> Option<BlackLevel> {
    self.blacklevel.as_ref().map(|x| {
      if x.len() == 1 {
        BlackLevel::new(&vec![x[0]; cpp], 1, 1, cpp)
      } else if x.len() == self.cfa.width * self.cfa.height * cpp {
        BlackLevel::new(x, self.cfa.width, self.cfa.height, cpp)
      } else {
        panic!("Invalid blacklevel data")
      }
    })
  }

  pub fn make_whitelevel(&self, cpp: usize) -> Option<WhiteLevel> {
    self.whitelevel.as_ref().map(|x| {
      if x.len() == 1 {
        WhiteLevel(vec![x[0] as u32; cpp])
      } else if x.len() == cpp {
        WhiteLevel(x.clone())
      } else {
        panic!("Invalid whitelevel data")
      }
    })
  }

  pub fn update_from_toml(&mut self, ct: &toml::value::Table) {
    for (name, val) in ct {
      match name.as_ref() {
        n @ "make" => {
          self.make = val.as_str().unwrap_or_else(|| panic!("{} must be a string", n)).to_string();
        }
        n @ "model" => {
          self.model = val.as_str().unwrap_or_else(|| panic!("{} must be a string", n)).to_string();
        }
        n @ "mode" => {
          self.mode = val.as_str().unwrap_or_else(|| panic!("{} must be a string", n)).to_string();
        }
        n @ "clean_make" => {
          self.clean_make = val.as_str().unwrap_or_else(|| panic!("{} must be a string", n)).to_string();
        }
        n @ "clean_model" => {
          self.clean_model = val.as_str().unwrap_or_else(|| panic!("{} must be a string", n)).to_string();
        }
        n @ "remark" => {
          self.remark = Some(val.as_str().unwrap_or_else(|| panic!("{} must be a string", n)).to_string());
        }
        n @ "whitepoint" => {
          let white = val.as_integer().unwrap_or_else(|| panic!("{} must be an integer", n)) as u32;
          self.whitelevel = Some(vec![white]);
        }
        n @ "blackpoint" => {
          let black = val.as_integer().unwrap_or_else(|| panic!("{} must be an integer", n)) as u32;
          self.blacklevel = Some(vec![black]);
        }
        n @ "blackareah" => {
          let vals = val.as_array().unwrap_or_else(|| panic!("{} must be an array", n));
          self.blackareah = Some((vals[0].as_integer().unwrap() as usize, vals[1].as_integer().unwrap() as usize));
        }
        n @ "blackareav" => {
          let vals = val.as_array().unwrap_or_else(|| panic!("{} must be an array", n));
          self.blackareav = Some((vals[0].as_integer().unwrap() as usize, vals[1].as_integer().unwrap() as usize));
        }
        "color_matrix" => {
          if let Some(color_matrix) = val.as_table() {
            for (illu_str, matrix) in color_matrix.into_iter() {
              let illu = Illuminant::new_from_str(illu_str).unwrap();
              let xyz_to_cam = matrix
                .as_array()
                .expect("color matrix must be array")
                .iter()
                .map(|a| a.as_float().expect("color matrix values must be float") as f32)
                .collect();
              self.color_matrix.insert(illu, xyz_to_cam);
            }
          } else {
            eprintln!("Invalid matrix spec for {}", self.clean_model);
          }
          assert!(!self.color_matrix.is_empty());
        }
        n @ "active_area" => {
          let crop_vals = val.as_array().unwrap_or_else(|| panic!("{} must be an array", n));
          let mut crop = [0, 0, 0, 0];
          for (i, val) in crop_vals.iter().enumerate() {
            crop[i] = val.as_integer().unwrap() as usize;
          }
          self.active_area = Some(crop);
        }
        n @ "crop_area" => {
          let crop_vals = val.as_array().unwrap_or_else(|| panic!("{} must be an array", n));
          let mut crop = [0, 0, 0, 0];
          for (i, val) in crop_vals.iter().enumerate() {
            crop[i] = val.as_integer().unwrap() as usize;
          }
          self.crop_area = Some(crop);
        }
        n @ "color_pattern" => {
          self.cfa = CFA::new(val.as_str().unwrap_or_else(|| panic!("{} must be a string", n)));
        }
        n @ "plane_color" => {
          self.plane_color = PlaneColor::new(val.as_str().unwrap_or_else(|| panic!("{} must be a string", n)));
        }
        n @ "bps" => {
          self.bps = Some(val.as_integer().unwrap_or_else(|| panic!("{} must be an integer", n)) as usize);
        }
        n @ "real_bps" => {
          self.real_bps = val.as_integer().unwrap_or_else(|| panic!("{} must be an integer", n)) as usize;
        }
        n @ "filesize" => {
          self.filesize = val.as_integer().unwrap_or_else(|| panic!("{} must be an integer", n)) as usize;
        }
        n @ "raw_width" => {
          self.raw_width = val.as_integer().unwrap_or_else(|| panic!("{} must be an integer", n)) as usize;
        }
        n @ "raw_height" => {
          self.raw_height = val.as_integer().unwrap_or_else(|| panic!("{} must be an integer", n)) as usize;
        }
        n @ "highres_width" => {
          self.highres_width = val.as_integer().unwrap_or_else(|| panic!("{} must be an integer", n)) as usize;
        }
        n @ "default_scale" => {
          let scale_vals = val.as_array().unwrap_or_else(|| panic!("{} must be an array", n));
          let scale_h = scale_vals[0].as_array().expect("must be array");
          let scale_v = scale_vals[1].as_array().expect("must be array");
          let scale = [
            [
              scale_h[0].as_integer().expect("must be integer") as u32,
              scale_h[1].as_integer().expect("must be integer") as u32,
            ],
            [
              scale_v[0].as_integer().expect("must be integer") as u32,
              scale_v[1].as_integer().expect("must be integer") as u32,
            ],
          ];
          self.default_scale = DefaultScale(scale);
        }
        n @ "best_quality_scale" => {
          let scale_vals = val.as_array().unwrap_or_else(|| panic!("{} must be an array", n));
          self.best_quality_scale = BestQualityScale([
            scale_vals[0].as_integer().expect("must be integer") as u32,
            scale_vals[1].as_integer().expect("must be integer") as u32,
          ]);
        }
        n @ "hints" => {
          self.hints = Vec::new();
          for hint in val.as_array().unwrap_or_else(|| panic!("{} must be an array", n)) {
            self.hints.push(hint.as_str().expect("hints must be a string").to_string());
          }
        }
        n @ "params" => {
          for (name, val) in val.as_table().unwrap_or_else(|| panic!("{} must be a table", n)) {
            self.params.insert(name.clone(), val.clone());
          }
        }
        "model_aliases" => {}
        "modes" => {} // ignore
        key => {
          panic!("Unknown key: {}", key);
        }
      }
    }
  }

  pub fn new() -> Camera {
    Camera {
      make: "".to_string(),
      model: "".to_string(),
      mode: "".to_string(),
      clean_make: "".to_string(),
      clean_model: "".to_string(),
      remark: None,
      filesize: 0,
      raw_width: 0,
      raw_height: 0,
      whitelevel: None,
      blacklevel: None,
      blackareah: None,
      blackareav: None,
      xyz_to_cam: [[0.0; 3]; 4],
      color_matrix: HashMap::new(),
      cfa: CFA::new(""),
      plane_color: PlaneColor::default(),
      active_area: None,
      crop_area: None,
      bps: None,
      real_bps: 16,
      highres_width: usize::MAX,
      default_scale: DefaultScale::default(),
      best_quality_scale: BestQualityScale::default(),
      hints: Vec::new(),
      params: HashMap::new(),
      //orientation: Orientation::Unknown,
    }
  }
}
