extern crate zip;

use self::zip::ZipArchive;
use super::*;
use std::io::Read;

const INVALID_VALUE_MESSAGE: &str =
  "Expected a color in format of #rrggbb or #rrggbbaa, or a variable's name";
const UNEXPECTED_COLON_MESSAGE: &str = "Unexpected colon (`:`)";
const UNEXPECTED_SEMICOLON_MESSAGE: &str = "Unexpected semicolon (`;`)";
const EXPECTED_COLON_MESSAGE: &str = "Expected a colon (`:`)";
const EXPECTED_SEMICOLON_MESSAGE: &str = "Expected a semicolon (`;`)";
const INVALID_NAME_MESSAGE: &str = "A variable's name may only consist of \
                                    latin letters, digits and the symbol `_`";
const NO_COLORS_DECLARATION_MESSAGE: &str =
  "A `.tdesktop-theme` archive must have a `colors.tdesktop-theme` file";
const BACKGROUND_PRECEDENCE: [(&str, WallpaperType, WallpaperExtension); 4] = [
  ("background.jpg", WallpaperType::Background, WallpaperExtension::Jpg),
  ("background.png", WallpaperType::Background, WallpaperExtension::Png),
  ("tiled.jpg", WallpaperType::Tiled, WallpaperExtension::Jpg),
  ("tiled.png", WallpaperType::Tiled, WallpaperExtension::Png),
];

const SHORT_HEX_LENGTH: usize = 1 + 2 * 3;
const LONG_HEX_LENGTH: usize = 1 + 2 * 4;

#[derive(PartialEq)]
struct Token<'a> {
  token: &'a [u8],
  line: usize,
  column: usize,
}

impl<'a> std::fmt::Debug for Token<'a> {
  fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
    write!(
      f,
      "Token \"{token}\" at {line}:{column}",
      token = String::from_utf8(self.token.to_vec()).unwrap(),
      line = self.line,
      column = self.column,
    )
  }
}

enum InComment {
  None,
  SingleLine,
  MultiLine,
}

/// Represents an error occured during parsing.
///
/// # Reasons parsing may fail
///
/// - Absence of the file `colors.tdesktop-theme` if the parser was given an
///   archive;
/// - Bad formatting of the palette file (it's `<variable>:<color>;`, with
///   any number of whitespaces between tokens, and also C-style comments);
/// - Wrong format of colors (they may only be `#rrggbb` or `#rrggbbaa`);
/// - Wrong format of variable names (they may only contain latin letters,
///   digits and the underscore symbol (`_`)).
#[derive(Debug)]
pub struct ParseError {
  /// Contains a message explaining why parsing failed.
  pub message: &'static str,
  /// If possible, the parser will report the line of the contents where parsing
  /// failed.
  pub line: Option<usize>,
  /// If possible, the parser will report the column of the contents where
  /// parsing failed.
  pub column: Option<usize>,
}

impl ParseError {
  fn new(message: &'static str, line: usize, column: usize) -> ParseError {
    ParseError {
      message,
      line: Some(line),
      column: Some(column),
    }
  }

  fn invalid_value(line: usize, column: usize) -> ParseError {
    ParseError::new(INVALID_VALUE_MESSAGE, line, column)
  }

  fn unexpected_colon(line: usize, column: usize) -> ParseError {
    ParseError::new(UNEXPECTED_COLON_MESSAGE, line, column)
  }

  fn unexpected_semicolon(line: usize, column: usize) -> ParseError {
    ParseError::new(UNEXPECTED_SEMICOLON_MESSAGE, line, column)
  }

  fn expected_colon(line: usize, column: usize) -> ParseError {
    ParseError::new(EXPECTED_COLON_MESSAGE, line, column)
  }

  fn expected_semicolon(line: usize, column: usize) -> ParseError {
    ParseError::new(EXPECTED_SEMICOLON_MESSAGE, line, column)
  }

  fn invalid_name(line: usize, column: usize) -> ParseError {
    ParseError::new(INVALID_NAME_MESSAGE, line, column)
  }

  fn no_colors_declaration() -> ParseError {
    ParseError {
      message: NO_COLORS_DECLARATION_MESSAGE,
      line: None,
      column: None,
    }
  }
}

impl std::fmt::Display for ParseError {
  fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
    if let Some(line) = self.line {
      write!(
        f,
        "{message} at {line}:{column}",
        message = self.message,
        line = line,
        // If `line` exists, then `columns` must exist too.
        column = self.column.unwrap(),
      )
    } else {
      write!(f, "{}", self.message)
    }
  }
}

impl std::error::Error for ParseError {}

fn parse_hex_number(
  number: &[u8],
  line: usize,
  column: usize,
) -> Result<u8, ParseError> {
  let mut result = 0;

  for (index, digit) in number.iter().enumerate() {
    let digit = *digit;
    let shift = (number.len() - index - 1) as u8 * 4;

    if digit >= b'0' && digit <= b'9' {
      result += (digit - b'0') << shift;
    } else if digit >= b'a' && digit <= b'f' {
      result += (digit - b'a' + 10) << shift;
    } else if digit >= b'A' && digit <= b'F' {
      result += (digit - b'A' + 10) << shift;
    } else {
      return Err(ParseError::invalid_value(line, column));
    }
  }

  Ok(result)
}

fn parse_color(
  value: &[u8],
  line: usize,
  column: usize,
) -> Result<Color, ParseError> {
  if value == b":" {
    return Err(ParseError::unexpected_colon(line, column));
  }

  if value == b";" {
    return Err(ParseError::unexpected_semicolon(line, column));
  }

  if (value.len() != SHORT_HEX_LENGTH && value.len() != LONG_HEX_LENGTH)
    || value[0] != b'#'
  {
    return Err(ParseError::invalid_value(line, column));
  }

  let red = parse_hex_number(&value[1..3], line, column)?;
  let green = parse_hex_number(&value[3..5], line, column)?;
  let blue = parse_hex_number(&value[5..7], line, column)?;
  let alpha = if value.len() == LONG_HEX_LENGTH {
    parse_hex_number(&value[7..9], line, column)?
  } else {
    0xff
  };

  Ok([red, green, blue, alpha])
}

fn tokenize(contents: &[u8]) -> Vec<Token> {
  let mut tokens = Vec::new();
  let mut line = 1;
  let mut column = 1;
  let mut is_in_comment = InComment::None;
  let mut token_start_index = None;
  let mut token_start_line = 0;
  let mut token_start_column = 0;

  let mut index = 0;

  macro_rules! skip {
    ($offset:expr) => {{
      index += $offset;
      column += $offset;
    }};
  }

  while let Some(symbol) = contents.get(index) {
    if symbol == &b'\n' {
      if let InComment::SingleLine = is_in_comment {
        is_in_comment = InComment::None;
      }

      line += 1;
      column = 1;
      index += 1;

      continue;
    }

    if let InComment::SingleLine = is_in_comment {
      skip!(1);
      continue;
    }

    if index + 2 < contents.len() {
      match &contents[index..index + 2] {
        b"//" => {
          is_in_comment = InComment::SingleLine;
          skip!(2);
          continue;
        }
        b"/*" => {
          is_in_comment = InComment::MultiLine;
          skip!(2);
          continue;
        }
        _ => (),
      }
    }

    if let InComment::MultiLine = is_in_comment {
      if index + 2 < contents.len() && &contents[index..index + 2] == b"*/" {
        is_in_comment = InComment::None;
        skip!(2);
      } else {
        skip!(1);
      }

      continue;
    }

    if (*symbol as char).is_whitespace() {
      if let Some(start) = token_start_index {
        tokens.push(Token {
          token: &contents[start..index],
          line: token_start_line,
          column: token_start_column,
        });
        token_start_index = None;
      }

      skip!(1);
      continue;
    }

    match symbol {
      b':' | b';' => {
        if let Some(start) = token_start_index {
          tokens.push(Token {
            token: &contents[start..index],
            line: token_start_line,
            column: token_start_column,
          });
        }

        tokens.push(Token {
          token: &contents[index..=index],
          line,
          column,
        });
        token_start_index = None;
      }
      _ => {
        if token_start_index.is_none() {
          token_start_index = Some(index);
          token_start_line = line;
          token_start_column = column;
        }
      }
    }

    skip!(1);
  }

  if let Some(start) = token_start_index {
    tokens.push(Token {
      token: &contents[start..index],
      line: token_start_line,
      column: token_start_column,
    });
  }

  tokens
}

fn get_name(
  bytes: &[u8],
  line: usize,
  column: usize,
) -> Result<String, ParseError> {
  if bytes == b":" {
    return Err(ParseError::unexpected_colon(line, column));
  }

  if bytes == b";" {
    return Err(ParseError::unexpected_semicolon(line, column));
  }

  let is_name_correct = bytes.iter().all(|symbol| {
    let symbol = *symbol;

    (symbol >= b'a' && symbol <= b'z')
      || (symbol >= b'A' && symbol <= b'Z')
      || (symbol >= b'0' && symbol <= b'9')
      || symbol == b'_'
  });

  if is_name_correct {
    Ok(String::from_utf8(bytes.to_vec()).unwrap())
  } else {
    Err(ParseError::invalid_name(line, column))
  }
}

fn parse_palette(contents: &[u8]) -> Result<Variables, ParseError> {
  let tokens = tokenize(contents);
  let mut variables = IndexMap::new();

  for tokens_group in tokens.chunks(4) {
    let Token {
      token: variable_name,
      line,
      column,
    } = tokens_group[0];
    let mut line = line;
    let mut column = column;

    if let Some(colon) = tokens_group.get(1) {
      if colon.token != b":" {
        return Err(ParseError::expected_colon(colon.line, colon.column));
      }

      line = colon.line;
      column = colon.column;
    } else {
      return Err(ParseError::expected_colon(line, column + 1));
    };

    let value = if let Some(color) = tokens_group.get(2) {
      color
    } else {
      return Err(ParseError::invalid_value(line, column));
    };

    line = value.line;
    column = value.column;

    if let Some(semicolon) = tokens_group.get(3) {
      if semicolon.token != b";" {
        return Err(ParseError::expected_semicolon(
          semicolon.line,
          semicolon.column,
        ));
      }
    } else {
      return Err(ParseError::expected_semicolon(line, column + 1));
    }

    let variable_name = get_name(&variable_name, line, column)?;
    let value = if let Ok(color) = parse_color(value.token, line, column) {
      Value::Color(color)
    } else if let Ok(name) = get_name(value.token, line, column) {
      Value::Link(name)
    } else {
      return Err(ParseError::invalid_value(line, column));
    };

    variables.insert(variable_name, value);
  }

  Ok(variables)
}

fn get_bytes(file: self::zip::read::ZipFile<'_>) -> Vec<u8> {
  file
    .bytes()
    // Any hint why reading a byte may fail in the middle?
    .map(|x| x.unwrap())
    .collect()
}

pub fn parse(contents: &[u8]) -> Result<TdesktopTheme, ParseError> {
  let cursor = std::io::Cursor::new(contents);

  if let Ok(mut archive) = ZipArchive::new(cursor) {
    let variables = {
      if let Ok(palette) = archive.by_name("colors.tdesktop-theme") {
        let bytes = get_bytes(palette);

        parse_palette(&bytes[..])?
      } else {
        return Err(ParseError::no_colors_declaration());
      }
    };

    let mut wallpaper = None;

    for (filename, wallpaper_type, extension) in BACKGROUND_PRECEDENCE.iter() {
      if let Ok(content) = archive.by_name(filename) {
        wallpaper = Some(Wallpaper {
          wallpaper_type: *wallpaper_type,
          extension: *extension,
          bytes: get_bytes(content),
        });

        break;
      }
    }

    Ok(TdesktopTheme {
      wallpaper,
      variables,
    })
  } else {
    Ok(TdesktopTheme {
      wallpaper: None,
      variables: parse_palette(contents)?,
    })
  }
}

#[cfg(test)]
mod tests {
  use super::*;
  use std::fs::read;

  #[test]
  fn correctly_parses_hex_numbers() {
    assert_eq!(parse_hex_number(b"00", 0, 0).unwrap(), 0x00);
    assert_eq!(parse_hex_number(b"05", 0, 0).unwrap(), 0x05);
    assert_eq!(parse_hex_number(b"0f", 0, 0).unwrap(), 0x0f);
    assert_eq!(parse_hex_number(b"50", 0, 0).unwrap(), 0x50);
    assert_eq!(parse_hex_number(b"55", 0, 0).unwrap(), 0x55);
    assert_eq!(parse_hex_number(b"5f", 0, 0).unwrap(), 0x5f);
    assert_eq!(parse_hex_number(b"f0", 0, 0).unwrap(), 0xf0);
    assert_eq!(parse_hex_number(b"f5", 0, 0).unwrap(), 0xf5);
    assert_eq!(parse_hex_number(b"ff", 0, 0).unwrap(), 0xff);
    assert_eq!(parse_hex_number(b"FF", 0, 0).unwrap(), 0xff);

    assert!(
      parse_hex_number(b"gg", 0, 0).is_err(),
      "parse_hex_number should've returned an error for `gg`",
    );

    assert!(
      parse_hex_number(b"GG", 0, 0).is_err(),
      "parse_hex_number should've returned an error for `GG`",
    );

    assert!(
      parse_hex_number(b"::", 0, 0).is_err(),
      "parse_hex_number should've returned an error for `::`",
    );

    assert!(
      parse_hex_number(b"!!", 0, 0).is_err(),
      "parse_hex_number should've returned an error for `!!`",
    );
  }

  #[test]
  fn correctly_parses_colors() {
    assert_eq!(
      parse_color(b"#102030", 0, 0).unwrap(),
      [0x10, 0x20, 0x30, 0xff],
    );

    assert_eq!(
      parse_color(b"#10203040", 0, 0).unwrap(),
      [0x10, 0x20, 0x30, 0x40],
    );

    assert!(
      parse_color(b"#fff", 0, 0).is_err(),
      "parse_color should've returned an error for `#fff`",
    );

    assert!(
      parse_color(b"1020304", 0, 0).is_err(),
      "parse_color should've returned an error for `1020304`",
    );
  }

  #[test]
  fn tokenizes_correctly() {
    assert_eq!(
      tokenize(
        b"windowBg: #123456; // this is a comment
        windowFg:#12345678; /*
          this is a multiline comment
          :;
        */ windowActiveBg : #112233;"
      ),
      vec![
        Token {
          token: b"windowBg",
          line: 1,
          column: 1
        },
        Token {
          token: b":",
          line: 1,
          column: 9
        },
        Token {
          token: b"#123456",
          line: 1,
          column: 11
        },
        Token {
          token: b";",
          line: 1,
          column: 18
        },
        Token {
          token: b"windowFg",
          line: 2,
          column: 9
        },
        Token {
          token: b":",
          line: 2,
          column: 17
        },
        Token {
          token: b"#12345678",
          line: 2,
          column: 18
        },
        Token {
          token: b";",
          line: 2,
          column: 27
        },
        Token {
          token: b"windowActiveBg",
          line: 5,
          column: 12
        },
        Token {
          token: b":",
          line: 5,
          column: 27
        },
        Token {
          token: b"#112233",
          line: 5,
          column: 29
        },
        Token {
          token: b";",
          line: 5,
          column: 36
        },
      ],
    );

    assert_eq!(
      tokenize(b"w"),
      vec![Token {
        token: b"w",
        line: 1,
        column: 1
      }],
    );
  }

  #[test]
  fn parses_palettes_correctly() {
    assert_eq!(
      parse_palette(
        b"windowBg: #ffffff;
        windowFg: #00000000;
        windowBoldFg: windowFg;"
      )
      .unwrap(),
      indexmap! {
        "windowBg".to_string() => Value::Color([0xff; 4]),
        "windowFg".to_string() => Value::Color([0x00; 4]),
        "windowBoldFg".to_string() => Value::Link("windowFg".to_string()),
      },
    );

    assert!(
      parse_palette(b";").is_err(),
      "parse_palette should've returned an error for `;`",
    );

    assert!(
      parse_palette(b":").is_err(),
      "parse_palette should've returned an error for `:`",
    );

    assert!(
      parse_palette(b"w").is_err(),
      "parse_palette should've returned an error for `w`",
    );

    assert!(
      parse_palette(b"w: #;").is_err(),
      "parse_palette should've returned an error for `w: #;`",
    );

    assert!(
      parse_palette(b"w: v").is_err(),
      "parse_palette should've returned an error for `w: v`",
    );
  }

  #[test]
  fn general_parser_works_correctly() {
    let contents = read("./tests/assets/palette.tdesktop-palette").unwrap();
    let theme = parse(&contents[..]).unwrap();

    assert_eq!(
      theme,
      TdesktopTheme {
        wallpaper: None,
        variables: indexmap! {
          "windowBg".to_string() => Value::Color([0x10, 0x20, 0x30, 0xff]),
        },
      },
    );

    let contents =
      read("./tests/assets/all-wallpapers.tdesktop-theme").unwrap();
    let theme = parse(&contents[..]).unwrap();

    assert_eq!(
      theme,
      TdesktopTheme {
        wallpaper: Some(Wallpaper {
          wallpaper_type: WallpaperType::Background,
          extension: WallpaperExtension::Jpg,
          bytes: Vec::new(),
        }),
        variables: IndexMap::new(),
      },
    );

    let contents =
      read("./tests/assets/no-background-jpg.tdesktop-theme").unwrap();
    let theme = parse(&contents[..]).unwrap();

    assert_eq!(
      theme,
      TdesktopTheme {
        wallpaper: Some(Wallpaper {
          wallpaper_type: WallpaperType::Background,
          extension: WallpaperExtension::Png,
          bytes: Vec::new(),
        }),
        variables: IndexMap::new(),
      },
    );

    let contents =
      read("./tests/assets/no-all-background.tdesktop-theme").unwrap();
    let theme = parse(&contents[..]).unwrap();

    assert_eq!(
      theme,
      TdesktopTheme {
        wallpaper: Some(Wallpaper {
          wallpaper_type: WallpaperType::Tiled,
          extension: WallpaperExtension::Jpg,
          bytes: Vec::new(),
        }),
        variables: IndexMap::new(),
      },
    );

    let contents =
      read("./tests/assets/only-tiled-png.tdesktop-theme").unwrap();
    let theme = parse(&contents[..]).unwrap();

    assert_eq!(
      theme,
      TdesktopTheme {
        wallpaper: Some(Wallpaper {
          wallpaper_type: WallpaperType::Tiled,
          extension: WallpaperExtension::Png,
          bytes: Vec::new(),
        }),
        variables: IndexMap::new(),
      },
    );

    let contents = read("./tests/assets/only-colors.tdesktop-theme").unwrap();
    let theme = parse(&contents[..]).unwrap();

    assert_eq!(
      theme,
      TdesktopTheme {
        wallpaper: None,
        variables: indexmap! {
          "windowBg".to_string() => Value::Color([0x10, 0x20, 0x30, 0xff]),
        },
      },
    );

    let contents = read("./tests/assets/no-colors.tdesktop-theme").unwrap();

    assert!(
      parse(&contents[..]).is_err(),
      "`parse` should've returned an error for a `.tdesktop-theme` file with \
       no `colors.tdesktop-theme`",
    );
  }
}
