use std::{collections::HashMap, fmt::Debug};

use base64::{engine::general_purpose::STANDARD, Engine};
use psdk_toolkit::hex::HexOutput;

use crate::{errors::PsdkFatherResult, quick, types::psdk_const};

pub trait PsdkCommand: Clone + Debug {
  fn binary(&self) -> &[u8];
  fn string(&self) -> PsdkFatherResult<String> {
    self.string_with_charset(psdk_const::DEF_CHARSET)
  }
  fn string_with_charset(&self, charset: impl AsRef<str>) -> PsdkFatherResult<String> {
    let bytes = self.binary();
    quick::quick_text_decode(bytes, charset)
  }
  fn hex(&self) -> String {
    let output = HexOutput::default();
    self.hex_with_output(output)
  }
  fn hex_with_output(&self, output: HexOutput) -> String {
    let bytes = self.binary();
    output.format(bytes)
  }
  fn base64(&self) -> String {
    let bytes = self.binary();
    STANDARD.encode(bytes)
  }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CommandClauseType {
  Text,
  Binary,
  Newline,
  None,
}

#[derive(Clone, Debug)]
pub struct CommandClause {
  pub binary: Option<Vec<u8>>,
  pub text: Option<String>,
  pub charset: Option<String>,
  pub type_: CommandClauseType,
}

impl CommandClause {
  pub fn with_binary(binary: &[u8]) -> Self {
    Self {
      binary: Some(binary.to_vec()),
      text: None,
      charset: None,
      type_: CommandClauseType::Binary,
    }
  }

  pub fn with_text(text: impl AsRef<str>) -> Self {
    Self {
      binary: None,
      text: Some(text.as_ref().to_string()),
      charset: None,
      type_: CommandClauseType::Text,
    }
  }

  pub fn with_text_and_charset(text: impl AsRef<str>, charset: impl AsRef<str>) -> Self {
    Self {
      binary: None,
      text: Some(text.as_ref().to_string()),
      charset: Some(charset.as_ref().to_string()),
      type_: CommandClauseType::Text,
    }
  }

  pub fn newline() -> Self {
    let mut that = Self::with_binary(psdk_const::DEF_NEWLINE_BINARY);
    that.type_ = CommandClauseType::Newline;
    that
  }

  pub fn none() -> Self {
    Self {
      binary: None,
      text: None,
      charset: None,
      type_: CommandClauseType::None,
    }
  }
}

#[derive(Clone, Debug)]
pub struct DefaultCommand {
  binary: Vec<u8>,
}

impl DefaultCommand {
  pub(crate) fn new(
    clauses: Vec<CommandClause>,
    variables: &HashMap<String, String>,
  ) -> PsdkFatherResult<Self> {
    let mut binary = vec![];
    for clause in clauses {
      match clause.type_ {
        CommandClauseType::Text => {
          let text = clause.text.expect("unreachable");
          let text = psdk_toolkit::variable::replace(&text, variables);
          if text.is_empty() {
            continue;
          }
          let charset = clause
            .charset
            .unwrap_or_else(|| psdk_const::DEF_CHARSET.to_string());
          let text_vec = quick::quick_text_encode(text, charset)?;
          binary.extend_from_slice(&text_vec);
        }
        CommandClauseType::Binary => {
          let vec = clause.binary.expect("unreachable");
          binary.extend_from_slice(&vec);
        }
        CommandClauseType::Newline => {
          let vec = clause.binary.expect("unreachable");
          binary.extend_from_slice(&vec);
        }
        CommandClauseType::None => continue,
      }
    }
    Ok(Self { binary })
  }
}

impl PsdkCommand for DefaultCommand {
  fn binary(&self) -> &[u8] {
    &self.binary
  }
}

#[cfg(test)]
mod tests {
  use std::collections::HashMap;

  use super::{CommandClause, DefaultCommand, PsdkCommand};

  #[test]
  fn test_default_command_replaces_text_variables_like_dart() {
    let variables = HashMap::from([("name".to_string(), "Alice".to_string())]);

    let command = DefaultCommand::new(
      vec![CommandClause::with_text("TEXT {{ name }}")],
      &variables,
    )
    .unwrap();

    assert_eq!(command.string().unwrap(), "TEXT Alice");
  }

  #[test]
  fn test_default_command_replaces_missing_text_variables_with_null_like_dart() {
    let variables = HashMap::new();

    let command = DefaultCommand::new(
      vec![CommandClause::with_text("TEXT {{ name }}")],
      &variables,
    )
    .unwrap();

    assert_eq!(command.string().unwrap(), "TEXT null");
  }
}
