use std::collections::{HashMap, HashSet};
use std::fmt;
use std::iter::FromIterator;
use lazy_static::lazy_static;
use regex::Regex;
use crate::{Dockerfile, Span, Splicer};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImageRef {
  
  pub registry: Option<String>,
  
  pub image: String,
  
  
  pub tag: Option<String>,
  
  pub hash: Option<String>
}
fn is_registry(token: &str) -> bool {
  token == "localhost" || token.contains('.') || token.contains(':')
}
fn substitute<'a, 'b>(
  s: &'a str,
  vars: &'b HashMap<&'b str, &'b str>,
  used_vars: &mut HashSet<String>,
  max_recursion_depth: u8
) -> Option<String> {
  lazy_static! {
    static ref VAR: Regex = Regex::new(r"\$(?:([A-Za-z0-9_]+)|\{([A-Za-z0-9_]+)\})").unwrap();
  }
  
  
  
  
  let mut splicer = Splicer::from_str(s);
  for caps in VAR.captures_iter(s) {
    if max_recursion_depth == 0 {
      
      return None;
    }
    let full_range = caps.get(0)?.range();
    let var_name = caps.get(1).or_else(|| caps.get(2))?;
    let var_content = vars.get(var_name.as_str())?;
    let substituted_content = substitute(
      var_content,
      vars,
      used_vars,
      max_recursion_depth.saturating_sub(1)
    )?;
    used_vars.insert(var_name.as_str().to_string());
    
    splicer.splice(&Span::new(full_range.start, full_range.end), &substituted_content);
  }
  Some(splicer.content)
}
impl ImageRef {
  
  
  
  
  pub fn parse(s: &str) -> ImageRef {
    
    
    
    
    
    
    
    
    
    
    
    
    let parts: Vec<&str> = s.splitn(2, '/').collect();
    let (registry, image_full) = if parts.len() == 2 && is_registry(parts[0]) {
      
      (Some(parts[0].to_string()), parts[1])
    } else {
      
      (None, s)
    };
    if let Some(at_pos) = image_full.find('@') {
      
      let (image, hash) = image_full.split_at(at_pos);
      ImageRef {
        registry,
        image: image.to_string(),
        hash: Some(hash[1..].to_string()),
        tag: None
      }
    } else {
      
      let parts: Vec<&str> = image_full.splitn(2, ':').collect();
      let image = parts[0].to_string();
      let tag = parts.get(1).map(|p| String::from(*p));
      ImageRef { registry, image, tag, hash: None }
    }
  }
  
  
  
  
  
  
  
  pub fn resolve_vars_with_context<'a>(
    &self, dockerfile: &'a Dockerfile
  ) -> Option<(ImageRef, HashSet<String>)> {
    let vars: HashMap<&'a str, &'a str> = HashMap::from_iter(
      dockerfile.global_args
        .iter()
        .filter_map(|a| match a.value.as_deref() {
          Some(v) => Some((a.name.as_str(), v)),
          None => None
        })
    );
    let mut used_vars = HashSet::new();
    if let Some(s) = substitute(&self.to_string(), &vars, &mut used_vars, 16) {
      Some((ImageRef::parse(&s), used_vars))
    } else {
      None
    }
  }
  
  
  
  
  
  
  
  pub fn resolve_vars(&self, dockerfile: &Dockerfile) -> Option<ImageRef> {
    self.resolve_vars_with_context(dockerfile).map(|(image, _vars)| image)
  }
}
impl fmt::Display for ImageRef {
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    if let Some(registry) = &self.registry {
      write!(f, "{}/", registry)?;
    }
    write!(f, "{}", self.image)?;
    if let Some(tag) = &self.tag {
      write!(f, ":{}", tag)?;
    } else if let Some(hash) = &self.hash {
      write!(f, "@{}", hash)?;
    }
    Ok(())
  }
}
#[cfg(test)]
mod tests {
  use super::*;
  use std::convert::TryInto;
  use indoc::indoc;
  use crate::instructions::*;
  #[test]
  fn test_image_parse_dockerhub() {
    assert_eq!(
      ImageRef::parse("alpine:3.10"),
      ImageRef {
        registry: None,
        image: "alpine".into(),
        tag: Some("3.10".into()),
        hash: None
      }
    );
    assert_eq!(
      ImageRef::parse("foo/bar"),
      ImageRef {
        registry: None,
        image: "foo/bar".into(),
        tag: None,
        hash: None
      }
    );
    assert_eq!(
      ImageRef::parse("clux/muslrust"),
      ImageRef {
        registry: None,
        image: "clux/muslrust".into(),
        tag: None,
        hash: None
      }
    );
    assert_eq!(
      ImageRef::parse("clux/muslrust:1.41.0-stable"),
      ImageRef {
        registry: None,
        image: "clux/muslrust".into(),
        tag: Some("1.41.0-stable".into()),
        hash: None
      }
    );
    assert_eq!(
      ImageRef::parse("fake_project/fake_image@fake_hash"),
      ImageRef {
        registry: None,
        image: "fake_project/fake_image".into(),
        tag: None,
        hash: Some("fake_hash".into())
      }
    );
    
    assert_eq!(
      ImageRef::parse("fake_project/fake_image@"),
      ImageRef {
        registry: None,
        image: "fake_project/fake_image".into(),
        tag: None,
        hash: Some("".into())
      }
    );
    assert_eq!(
      ImageRef::parse("fake_project/fake_image@sha256:"),
      ImageRef {
        registry: None,
        image: "fake_project/fake_image".into(),
        tag: None,
        hash: Some("sha256:".into())
      }
    );
  }
  #[test]
  fn test_image_parse_registry() {
    assert_eq!(
      ImageRef::parse("quay.io/prometheus/node-exporter:v0.18.1"),
      ImageRef {
        registry: Some("quay.io".into()),
        image: "prometheus/node-exporter".into(),
        tag: Some("v0.18.1".into()),
        hash: None
      }
    );
    assert_eq!(
      ImageRef::parse("gcr.io/fake_project/fake_image:fake_tag"),
      ImageRef {
        registry: Some("gcr.io".into()),
        image: "fake_project/fake_image".into(),
        tag: Some("fake_tag".into()),
        hash: None
      }
    );
    assert_eq!(
      ImageRef::parse("gcr.io/fake_project/fake_image"),
      ImageRef {
        registry: Some("gcr.io".into()),
        image: "fake_project/fake_image".into(),
        tag: None,
        hash: None
      }
    );
    assert_eq!(
      ImageRef::parse("gcr.io/fake_image"),
      ImageRef {
        registry: Some("gcr.io".into()),
        image: "fake_image".into(),
        tag: None,
        hash: None
      }
    );
    assert_eq!(
      ImageRef::parse("gcr.io/fake_image:fake_tag"),
      ImageRef {
        registry: Some("gcr.io".into()),
        image: "fake_image".into(),
        tag: Some("fake_tag".into()),
        hash: None
      }
    );
    assert_eq!(
      ImageRef::parse("quay.io/fake_project/fake_image@fake_hash"),
      ImageRef {
        registry: Some("quay.io".into()),
        image: "fake_project/fake_image".into(),
        tag: None,
        hash: Some("fake_hash".into())
      }
    );
  }
  #[test]
  fn test_image_parse_localhost() {
    assert_eq!(
      ImageRef::parse("localhost/foo"),
      ImageRef {
        registry: Some("localhost".into()),
        image: "foo".into(),
        tag: None,
        hash: None
      }
    );
    assert_eq!(
      ImageRef::parse("localhost/foo:bar"),
      ImageRef {
        registry: Some("localhost".into()),
        image: "foo".into(),
        tag: Some("bar".into()),
        hash: None
      }
    );
    assert_eq!(
      ImageRef::parse("localhost/foo/bar"),
      ImageRef {
        registry: Some("localhost".into()),
        image: "foo/bar".into(),
        tag: None,
        hash: None
      }
    );
    assert_eq!(
      ImageRef::parse("localhost/foo/bar:baz"),
      ImageRef {
        registry: Some("localhost".into()),
        image: "foo/bar".into(),
        tag: Some("baz".into()),
        hash: None
      }
    );
  }
  #[test]
  fn test_image_parse_registry_port() {
    assert_eq!(
      ImageRef::parse("example.com:1234/foo"),
      ImageRef {
        registry: Some("example.com:1234".into()),
        image: "foo".into(),
        tag: None,
        hash: None
      }
    );
    assert_eq!(
      ImageRef::parse("example.com:1234/foo:bar"),
      ImageRef {
        registry: Some("example.com:1234".into()),
        image: "foo".into(),
        tag: Some("bar".into()),
        hash: None
      }
    );
    assert_eq!(
      ImageRef::parse("example.com:1234/foo/bar"),
      ImageRef {
        registry: Some("example.com:1234".into()),
        image: "foo/bar".into(),
        tag: None,
        hash: None
      }
    );
    assert_eq!(
      ImageRef::parse("example.com:1234/foo/bar:baz"),
      ImageRef {
        registry: Some("example.com:1234".into()),
        image: "foo/bar".into(),
        tag: Some("baz".into()),
        hash: None
      }
    );
    
    
    assert_eq!(
      ImageRef::parse("example.com:1234/foo/bar/baz:qux"),
      ImageRef {
        registry: Some("example.com:1234".into()),
        image: "foo/bar/baz".into(),
        tag: Some("qux".into()),
        hash: None
      }
    );
  }
  #[test]
  fn test_substitute() {
    let mut vars = HashMap::new();
    vars.insert("foo", "bar");
    vars.insert("baz", "qux");
    vars.insert("lorem", "$foo");
    vars.insert("ipsum", "${lorem}");
    vars.insert("recursion1", "$recursion2");
    vars.insert("recursion2", "$recursion1");
    let mut used_vars = HashSet::new();
    assert_eq!(
      substitute("hello world", &vars, &mut used_vars, 16).as_deref(),
      Some("hello world")
    );
    let mut used_vars = HashSet::new();
    assert_eq!(
      substitute("hello $foo", &vars, &mut used_vars, 16).as_deref(),
      Some("hello bar")
    );
    assert_eq!(used_vars, {
      let mut h = HashSet::new();
      h.insert("foo".to_string());
      h
    });
    let mut used_vars = HashSet::new();
    assert_eq!(
      substitute("hello $foo", &vars, &mut used_vars, 0).as_deref(),
      None
    );
    assert!(used_vars.is_empty());
    let mut used_vars = HashSet::new();
    assert_eq!(
      substitute("hello ${foo}", &vars, &mut used_vars, 16).as_deref(),
      Some("hello bar")
    );
    assert_eq!(used_vars, {
      let mut h = HashSet::new();
      h.insert("foo".to_string());
      h
    });
    let mut used_vars = HashSet::new();
    assert_eq!(
      substitute("$baz $foo", &vars, &mut used_vars, 16).as_deref(),
      Some("qux bar")
    );
    assert_eq!(used_vars, {
      let mut h = HashSet::new();
      h.insert("baz".to_string());
      h.insert("foo".to_string());
      h
    });
    let mut used_vars = HashSet::new();
    assert_eq!(
      substitute("hello $lorem", &vars, &mut used_vars, 16).as_deref(),
      Some("hello bar")
    );
    assert_eq!(used_vars, {
      let mut h = HashSet::new();
      h.insert("foo".to_string());
      h.insert("lorem".to_string());
      h
    });
    let mut used_vars = HashSet::new();
    assert_eq!(
      substitute("hello $lorem", &vars, &mut used_vars, 1).as_deref(),
      None
    );
    assert!(used_vars.is_empty());
    let mut used_vars = HashSet::new();
    assert_eq!(
      substitute("hello $ipsum", &vars, &mut used_vars, 16).as_deref(),
      Some("hello bar")
    );
    assert_eq!(used_vars, {
      let mut h = HashSet::new();
      h.insert("foo".to_string());
      h.insert("lorem".to_string());
      h.insert("ipsum".to_string());
      h
    });
    let mut used_vars = HashSet::new();
    assert_eq!(
      substitute("hello $ipsum", &vars, &mut used_vars, 2).as_deref(),
      None
    );
    assert!(used_vars.is_empty());
    let mut used_vars = HashSet::new();
    assert_eq!(
      substitute("hello $recursion1", &vars, &mut used_vars, 16).as_deref(),
      None
    );
    assert!(used_vars.is_empty());
  }
  #[test]
  fn test_resolve_vars() {
    let d = Dockerfile::parse(indoc!(r#"
      ARG image=alpine:3.12
      FROM $image
    "#)).unwrap();
    let from: &FromInstruction = d.instructions
      .get(1).unwrap()
      .try_into().unwrap();
    assert_eq!(
      from.image_parsed.resolve_vars(&d),
      Some(ImageRef::parse("alpine:3.12"))
    );
  }
  #[test]
  fn test_resolve_vars_nested() {
    let d = Dockerfile::parse(indoc!(r#"
      ARG image=alpine
      ARG unnecessarily_nested=${image}
      ARG tag=3.12
      FROM ${unnecessarily_nested}:${tag}
    "#)).unwrap();
    let from: &FromInstruction = d.instructions
      .get(3).unwrap()
      .try_into().unwrap();
    assert_eq!(
      from.image_parsed.resolve_vars(&d),
      Some(ImageRef::parse("alpine:3.12"))
    );
  }
  #[test]
  fn test_resolve_vars_technically_invalid() {
    
    let d = Dockerfile::parse(indoc!(r#"
      ARG image
      FROM $image
    "#)).unwrap();
    let from: &FromInstruction = d.instructions
      .get(1).unwrap()
      .try_into().unwrap();
    assert_eq!(
      from.image_parsed.resolve_vars(&d),
      None
    );
  }
  #[test]
  fn test_resolve_vars_typo() {
    
    let d = Dockerfile::parse(indoc!(r#"
      ARG image="alpine:3.12"
      FROM $foo
    "#)).unwrap();
    let from: &FromInstruction = d.instructions
      .get(1).unwrap()
      .try_into().unwrap();
    assert_eq!(
      from.image_parsed.resolve_vars(&d),
      None
    );
  }
  #[test]
  fn test_resolve_vars_out_of_order() {
    
    let d = Dockerfile::parse(indoc!(r#"
      FROM $image
      ARG image="alpine:3.12"
    "#)).unwrap();
    let from: &FromInstruction = d.instructions
      .get(0).unwrap()
      .try_into().unwrap();
    assert_eq!(
      from.image_parsed.resolve_vars(&d),
      None
    );
  }
}