use crate::grid::Cell;
use crate::puzzle::Puzzle;

/// Serialize a puzzle to Across Lite .puz binary format.
/// Spec: https://code.google.com/archive/p/puz/wikis/FileFormat.wiki
pub fn to_puz(puzzle: &Puzzle, title: &str, author: &str) -> Vec<u8> {
    let w = puzzle.width as u8;
    let h = puzzle.height as u8;
    let num_clues = (puzzle.clues_across.len() + puzzle.clues_down.len()) as u16;

    // build solution and player state strings
    let mut solution = Vec::with_capacity(puzzle.width * puzzle.height);
    let mut state = Vec::with_capacity(puzzle.width * puzzle.height);
    for r in 0..puzzle.height {
        for c in 0..puzzle.width {
            match puzzle.grid.get(r, c) {
                Cell::Black => {
                    solution.push(b'.');
                    state.push(b'.');
                }
                Cell::Letter(ch) => {
                    solution.push(ch as u8);
                    state.push(b'-');
                }
                Cell::Empty => {
                    solution.push(b'A');
                    state.push(b'-');
                }
            }
        }
    }

    // build clue list in .puz order: sorted by clue number, across before down for same number
    let mut clues: Vec<(u32, &str, bool)> = Vec::new();
    for c in &puzzle.clues_across {
        clues.push((c.number, &c.text, true));
    }
    for c in &puzzle.clues_down {
        clues.push((c.number, &c.text, false));
    }
    clues.sort_by_key(|(num, _, is_across)| (*num, !*is_across));

    let clue_strings: Vec<&str> = clues.iter().map(|(_, text, _)| *text).collect();

    // build the file
    let mut buf = Vec::new();

    // header checksum placeholder (2 bytes) - will fill in later
    let cksum_pos = buf.len();
    buf.extend_from_slice(&[0u8; 2]);

    // file magic "ACROSS&DOWN\0"
    buf.extend_from_slice(b"ACROSS&DOWN\0");

    // overall checksum placeholder (2 bytes)
    buf.extend_from_slice(&[0u8; 2]);

    // masked checksums (8 bytes) - computed later against the spec's "ICHEATED" mask
    buf.extend_from_slice(&[0u8; 8]);

    // version string "1.3\0"
    buf.extend_from_slice(b"1.3\0");

    // reserved (2 bytes)
    buf.extend_from_slice(&[0u8; 2]);

    // scrambled checksum (2 bytes, 0 = unscrambled)
    buf.extend_from_slice(&[0u8; 2]);

    // reserved (12 bytes)
    buf.extend_from_slice(&[0u8; 12]);

    // width, height
    buf.push(w);
    buf.push(h);

    // number of clues (little-endian u16)
    buf.extend_from_slice(&num_clues.to_le_bytes());

    // puzzle type (1 = normal, little-endian u16)
    buf.extend_from_slice(&1u16.to_le_bytes());

    // solution type (0 = normal, little-endian u16)
    buf.extend_from_slice(&0u16.to_le_bytes());

    // solution
    buf.extend_from_slice(&solution);

    // player state
    buf.extend_from_slice(&state);

    // strings section: title, author, copyright, clues, notes - all null-terminated
    buf.extend_from_slice(title.as_bytes());
    buf.push(0);

    buf.extend_from_slice(author.as_bytes());
    buf.push(0);

    // copyright
    buf.push(0);

    // clues (null-terminated each)
    for clue in &clue_strings {
        buf.extend_from_slice(clue.as_bytes());
        buf.push(0);
    }

    // notes
    buf.push(0);

    // now compute checksums (per the puz format spec: CIB, grid strings, and the
    // four other checksums XOR-masked against "ICHEATED")

    // CIB: eight header bytes from width through the solution type (0x2C..0x34)
    let c_cib = cksum_region(&buf[0x2C..0x34]);
    let c_sol = cksum_region(&solution);
    let c_grid = cksum_region(&state);

    // partial string checksum: title/author/copyright/notes include their NUL,
    // clue texts do not
    let mut c_part = 0u16;
    for s in [title, author] {
        if !s.is_empty() {
            c_part = cksum_region_with_init(s.as_bytes(), c_part);
            c_part = cksum_region_with_init(&[0u8], c_part);
        }
    }
    for clue in &clue_strings {
        c_part = cksum_region_with_init(clue.as_bytes(), c_part);
    }

    // masked low/high checksum bytes at 0x10..0x17
    let magic = b"ICHEATED";
    let parts = [c_cib, c_sol, c_grid, c_part];
    let masked_pos = 0x10usize;
    for i in 0..4usize {
        buf[masked_pos + i] = magic[i] ^ (parts[i] & 0xFF) as u8;
        buf[masked_pos + 4 + i] = magic[4 + i] ^ ((parts[i] & 0xFF00) >> 8) as u8;
    }

    // overall checksum: CIB, solution, state, strings, then the masked bytes
    let mut cksum = c_cib;
    cksum = cksum_region_with_init(&solution, cksum);
    cksum = cksum_region_with_init(&state, cksum);
    for s in [title, author] {
        if !s.is_empty() {
            cksum = cksum_region_with_init(s.as_bytes(), cksum);
            cksum = cksum_region_with_init(&[0u8], cksum);
        }
    }
    for clue in &clue_strings {
        cksum = cksum_region_with_init(clue.as_bytes(), cksum);
    }
    cksum = cksum_region_with_init(&buf[0x10..0x18], cksum);

    // write checksums
    buf[cksum_pos] = (cksum & 0xFF) as u8;
    buf[cksum_pos + 1] = ((cksum >> 8) & 0xFF) as u8;
    buf[0x0E] = (c_cib & 0xFF) as u8;
    buf[0x0F] = ((c_cib >> 8) & 0xFF) as u8;

    buf
}

fn cksum_region(data: &[u8]) -> u16 {
    cksum_region_with_init(data, 0)
}

fn cksum_region_with_init(data: &[u8], init: u16) -> u16 {
    let mut cksum = init;
    for &b in data {
        if cksum & 1 != 0 {
            cksum = (cksum >> 1) + 0x8000;
        } else {
            cksum >>= 1;
        }
        cksum = cksum.wrapping_add(b as u16);
    }
    cksum
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::SymmetryType;
    use crate::grid::slot::Direction;
    use crate::grid::{Cell, Grid};
    use crate::puzzle::{Clue, PlacedWord, Puzzle};

    fn mini_puzzle() -> Puzzle {
        let mut grid = Grid::new(2, 2);
        grid.set(0, 0, Cell::Letter('A'));
        grid.set(0, 1, Cell::Letter('B'));
        grid.set(1, 0, Cell::Letter('C'));
        grid.set(1, 1, Cell::Letter('D'));
        let words = vec![
            PlacedWord { word: "AB".into(), row: 0, col: 0, direction: Direction::Across, clue_number: 1 },
            PlacedWord { word: "CD".into(), row: 1, col: 0, direction: Direction::Across, clue_number: 3 },
            PlacedWord { word: "AC".into(), row: 0, col: 0, direction: Direction::Down, clue_number: 1 },
            PlacedWord { word: "BD".into(), row: 0, col: 1, direction: Direction::Down, clue_number: 2 },
        ];
        let clue = |number: u32, direction: Direction, text: &str, answer: &str| Clue {
            number,
            direction,
            text: text.into(),
            answer: answer.into(),
            definition: None,
            wordplay_type: None,
        };
        Puzzle {
            grid,
            words,
            clues_across: vec![
                clue(1, Direction::Across, "First row", "AB"),
                clue(3, Direction::Across, "Second row", "CD"),
            ],
            clues_down: vec![
                clue(1, Direction::Down, "First col", "AC"),
                clue(2, Direction::Down, "Second col", "BD"),
            ],
            width: 2,
            height: 2,
            symmetry: SymmetryType::None,
            difficulty_score: 50,
            generation_time_ms: 0,
            theme: None,
            theme_entries: vec![],
            is_cryptic: false,
        }
    }

    #[test]
    fn checksums_follow_the_format_spec() {
        let title = "TP";
        let author = "AU";
        let buf = to_puz(&mini_puzzle(), title, author);

        let w = buf[0x2C] as usize;
        let h = buf[0x2D] as usize;
        let sol = &buf[0x34..0x34 + w * h];
        let state = &buf[0x34 + w * h..0x34 + 2 * w * h];

        let c_cib = cksum_region(&buf[0x2C..0x34]);
        let c_sol = cksum_region(sol);
        let c_grid = cksum_region(state);

        // clue texts in .puz order: sorted by number, across before down
        let clue_texts = ["First row", "First col", "Second col", "Second row"];
        let mut c_part = 0u16;
        for s in [title, author] {
            c_part = cksum_region_with_init(s.as_bytes(), c_part);
            c_part = cksum_region_with_init(&[0u8], c_part);
        }
        for c in clue_texts {
            c_part = cksum_region_with_init(c.as_bytes(), c_part);
        }

        // masked region, XORed against "ICHEATED"
        let magic = b"ICHEATED";
        let parts = [c_cib, c_sol, c_grid, c_part];
        for i in 0..4usize {
            assert_eq!(buf[0x10 + i], magic[i] ^ (parts[i] & 0xFF) as u8, "masked low {}", i);
            assert_eq!(buf[0x14 + i], magic[4 + i] ^ ((parts[i] & 0xFF00) >> 8) as u8, "masked high {}", i);
        }

        // overall checksum
        let mut expect = c_cib;
        expect = cksum_region_with_init(sol, expect);
        expect = cksum_region_with_init(state, expect);
        for s in [title, author] {
            expect = cksum_region_with_init(s.as_bytes(), expect);
            expect = cksum_region_with_init(&[0u8], expect);
        }
        for c in clue_texts {
            expect = cksum_region_with_init(c.as_bytes(), expect);
        }
        expect = cksum_region_with_init(&buf[0x10..0x18], expect);

        assert_eq!(u16::from_le_bytes([buf[0], buf[1]]), expect, "overall");
        assert_eq!(u16::from_le_bytes([buf[0x0E], buf[0x0F]]), c_cib, "cib");
    }
}
