use std::array;
use std::process::ChildStdin;
use std::process::Command;
use std::process::Stdio;
use std::io::Write;
use std::ops::Range;

const CDDA_FRAME_BYTES: usize = 2352;
const CDG_FRAME_BYTES: usize = 96;
const TOC_DESCRIPTOR_SIZE: usize = 8;

fn main() {
    let args: Vec<_> = std::env::args_os().collect();

    if args.len() != 9 && args.len() != 10 {
        eprintln!(
            "usage: cdg-decode disc.img disc.cdg disc.toc out-tmp.raw ffmpeg.exe out.mp4 start_track end_track mode"
        );
        return;
    }

    let img_file_name = &args[1];
    let cdg_file_name = &args[2];
    let toc_file_name = &args[3];
    let out_raw_file_name = &args[4];
    let ffmpeg_exe_file_name = &args[5];
    let out_mp4_file_name = &args[6];
    let start_track_number = args[7]
        .to_str()
        .unwrap()
        .parse::<usize>()
        .unwrap()
        .checked_sub(1)
        .unwrap();
    let end_track_number = args[8]
        .to_str()
        .unwrap()
        .parse::<usize>()
        .unwrap()
        .checked_sub(1)
        .unwrap();
    let cdeg_enabled = match args.get(9).map(|s| s.to_str()) {
        None | Some(Some("cdg")) => false,
        Some(Some("cdeg")) => true,
        Some(None) => {
            panic!("bad unicode in mode");
        }
        Some(Some(mode)) => {
            panic!("unknown mode {mode:?}, expected \"cdg\" or \"cdeg\"");
        }
    };

    let img = std::fs::read(img_file_name).expect("read img file");
    assert_eq!(img.len() % CDDA_FRAME_BYTES, 0);
    let img_frames = img.len() / CDDA_FRAME_BYTES;

    let cdg = std::fs::read(cdg_file_name).expect("read cdg file");
    assert_eq!(cdg.len() % CDG_FRAME_BYTES, 0);
    let cdg_frames = cdg.len() / CDG_FRAME_BYTES;
    let toc = std::fs::read(toc_file_name).expect("read toc file");
    let track_lba_ranges = parse_track_lba_ranges(&toc);
    assert!(!track_lba_ranges.is_empty());
    let disc_end_lba = track_lba_ranges.last().unwrap().end;
    assert!(disc_end_lba <= img_frames);
    assert!(disc_end_lba <= cdg_frames);

    let start_track = &track_lba_ranges[start_track_number];
    let end_track = &track_lba_ranges[end_track_number];

    assert!(!std::fs::exists(out_raw_file_name).unwrap(), "refusing to overwrite existing temp raw audio output");

    // TODO also pipe?
    std::fs::write(
        out_raw_file_name,
        &img[start_track.start * CDDA_FRAME_BYTES..end_track.end * CDDA_FRAME_BYTES],
    )
    .expect("write raw audio");

    let pixels_wide = RENDERED_H_BORDER_PIXELS * 2 + RENDERED_H_RANGE.len() * 6;
    let pixels_high = RENDERED_V_BORDER_PIXELS * 2 + RENDERED_V_RANGE.len() * 12;

    assert!(!std::fs::exists(out_mp4_file_name).unwrap(), "refusing to overwrite existing mp4 output");

    let mut child = Command::new(ffmpeg_exe_file_name)
        //.arg("-y")
        .arg("-f").arg("rawvideo")
        .arg("-pix_fmt").arg("rgb24")
        .arg("-s").arg(format!("{pixels_wide}x{pixels_high}"))
        .arg("-r").arg("60")
        .arg("-i").arg("-")
        .stdin(Stdio::piped())
        .arg("-f").arg("s16le")
        .arg("-ar").arg("44100")
        .arg("-ac").arg("2")
        .arg("-i").arg(out_raw_file_name)
        .arg("-vcodec").arg("libx264")
        //.arg("-vf").arg("format=yuv420p")
        .arg("-vf").arg("scale=624:432:flags=neighbor,format=yuv420p")
        .arg("-movflags").arg("+faststart")
        .arg(out_mp4_file_name)
        .spawn()
        .expect("spawn ffmpeg child");

    let mut child_stdin = child.stdin.take().expect("child stdin handle present");

    decode_cdg(
        &mut child_stdin,
        &cdg[start_track.start * CDG_FRAME_BYTES..end_track.end * CDG_FRAME_BYTES],
        cdeg_enabled,
    );

    child.stdin = Some(child_stdin);
    child.wait().expect("wait on child process");

    std::fs::remove_file(out_raw_file_name).expect("delete temp raw audio output");

}

fn parse_track_lba_ranges(toc: &[u8]) -> Vec<Range<usize>> {
    fn ra<const N: usize>(b: &[u8], offset: usize) -> [u8; N] {
        b[offset..offset + N].try_into().unwrap()
    }
    let data_length: usize = u16::from_be_bytes(ra(toc, 0)).into();
    assert_eq!(data_length + 2, toc.len());
    let track_count = (data_length - 2) / TOC_DESCRIPTOR_SIZE;
    assert!(track_count > 0);
    assert_eq!(track_count * TOC_DESCRIPTOR_SIZE + 4, toc.len());

    let first_descriptor_offset = 4;
    let track_aa_toc_offset = first_descriptor_offset + (track_count - 1) * TOC_DESCRIPTOR_SIZE;
    let track_aa_number = toc[track_aa_toc_offset + 2];
    assert_eq!(track_aa_number, 0xaa);

    (1..track_count)
        .map(|track_i| {
            let track_toc_offset = first_descriptor_offset + (track_i - 1) * TOC_DESCRIPTOR_SIZE;
            let next_track_toc_offset = first_descriptor_offset + track_i * TOC_DESCRIPTOR_SIZE;
            let track_number = toc[track_toc_offset + 2];
            assert_eq!(usize::from(track_number), track_i);

            let start_lba = u32::from_be_bytes(ra(toc, track_toc_offset + 4))
                .try_into()
                .expect("usize should hold u32");
            let end_lba = u32::from_be_bytes(ra(toc, next_track_toc_offset + 4))
                .try_into()
                .expect("usize should hold u32");

            start_lba..end_lba
        })
        .collect()
}

const CDG_CMD_LEN: usize = 24;

const CDG_BUFFER_TILES_WIDE: usize = 50;
const CDG_BUFFER_TILES_HIGH: usize = 18;
const CDG_CLUT_ENTRIES: usize = 16;
const CDG_CHANNELS: usize = 16;

struct CdgState {
    buffer: [[CdgTile; CDG_BUFFER_TILES_WIDE]; CDG_BUFFER_TILES_HIGH],
    border_color: usize,
    fine_scroll_x: usize,
    fine_scroll_y: usize,
    offset_x: usize,
    offset_y: usize,
    clut: [CdgColor; CDG_CLUT_ENTRIES],
    active_channels: [bool; CDG_CHANNELS],
}

impl Default for CdgState {
    fn default() -> Self {
        Self {
            buffer: [[CdgTile::default(); CDG_BUFFER_TILES_WIDE]; CDG_BUFFER_TILES_HIGH],
            border_color: 0,
            fine_scroll_x: 0,
            fine_scroll_y: 0,
            offset_x: 0,
            offset_y: 0,
            clut: [CdgColor::default(); CDG_CLUT_ENTRIES],
            active_channels: array::from_fn(|channel| channel == 0 || channel == 1),
        }
    }
}

const RENDERED_H_BORDER_PIXELS: usize = 12;
const RENDERED_H_RANGE: Range<usize> = 1..CDG_BUFFER_TILES_WIDE - 1;
const RENDERED_V_BORDER_PIXELS: usize = 12;
const RENDERED_V_RANGE: Range<usize> = 1..CDG_BUFFER_TILES_HIGH - 1;

impl CdgState {
    fn exec(&mut self, command: &[u8; CDG_CMD_LEN]) {
        match command[1] {
            1 => self.memory_preset_cmd(command),
            2 => self.border_preset_cmd(command),
            6 => self.font_cmd(command, false),
            38 => self.font_cmd(command, true /* xor */),
            20 => self.scroll_cmd(command, true /* preset */),
            24 => self.scroll_cmd(command, false),
            28 => {
                // set key color, ignored
            }
            30 => self.load_clut_cmd(command, 0),
            31 => self.load_clut_cmd(command, 8),
            _ => {
                // unknown command, ignored
            }
        }
    }

    fn memory_preset_cmd(&mut self, cmd_bytes: &[u8; CDG_CMD_LEN]) {
        self.buffer =
            [[CdgTile::new(cmd_bytes[4] & 0xf); CDG_BUFFER_TILES_WIDE]; CDG_BUFFER_TILES_HIGH];
        self.fine_scroll_x = 0;
        self.fine_scroll_y = 0;
    }

    fn border_preset_cmd(&mut self, cmd_bytes: &[u8; CDG_CMD_LEN]) {
        self.border_color = usize::from(cmd_bytes[4] & 0xf);
    }

    fn load_clut_cmd(&mut self, cmd_bytes: &[u8; CDG_CMD_LEN], load_start_index: usize) {
        assert!(load_start_index == 0 || load_start_index == 8);

        for i in 0..8 {
            let clut_index = load_start_index + i;
            self.clut[clut_index] = CdgColor::from_rgb4_cmd_bytes(cmd_bytes, i);
        }
    }

    fn font_cmd(&mut self, cmd_bytes: &[u8; CDG_CMD_LEN], xor: bool) {
        let channel = ((cmd_bytes[4] & 0b11_0000) >> 2) | ((cmd_bytes[5] & 0b11_0000) >> 4);
        if !self.active_channels[usize::from(channel)] {
            return;
        }

        let c0 = cmd_bytes[4] & 0xf;
        let c1 = cmd_bytes[5] & 0xf;
        let y = usize::from(cmd_bytes[6] & 0x1f);
        let x = usize::from(cmd_bytes[7] & 0x3f);
        if x >= CDG_BUFFER_TILES_WIDE {
            return;
        }
        if y >= CDG_BUFFER_TILES_HIGH {
            return;
        }

        let x = (x + self.offset_x) % CDG_BUFFER_TILES_WIDE;
        let y = (y + self.offset_y) % CDG_BUFFER_TILES_HIGH;

        if xor {
            self.buffer[y][x].xor_cmd(c0, c1, cmd_bytes);
        } else {
            self.buffer[y][x].set_cmd(c0, c1, cmd_bytes);
        }
    }

    fn scroll_cmd(&mut self, cmd_bytes: &[u8; CDG_CMD_LEN], preset: bool) {
        let preset_color = cmd_bytes[4] & 0xf;

        {
            let h_cmd = cmd_bytes[5] & 0x3f;
            let (h_delta, h_preset_column) = match h_cmd >> 4 {
                1 => (CDG_BUFFER_TILES_WIDE - 1, 0),
                2 => (1, CDG_BUFFER_TILES_WIDE - 1),
                _ => (0, 0),
            };

            self.offset_x = (self.offset_x + h_delta) % CDG_BUFFER_TILES_WIDE;

            if preset && h_delta != 0 {
                let x = (self.offset_x + h_preset_column) % CDG_BUFFER_TILES_WIDE;
                for y in 0..CDG_BUFFER_TILES_HIGH {
                    self.buffer[y][x] = CdgTile::new(preset_color);
                }
            }

            self.fine_scroll_x = std::cmp::min(usize::from(h_cmd & 0x7), 5);
        }

        {
            let v_cmd = cmd_bytes[6] & 0x3f;

            let (v_delta, v_preset_row) = match v_cmd >> 4 {
                1 => (CDG_BUFFER_TILES_HIGH - 1, 0),
                2 => (1, CDG_BUFFER_TILES_HIGH - 1),
                _ => (0, 0),
            };

            self.offset_y = (self.offset_y + v_delta) % CDG_BUFFER_TILES_HIGH;

            if preset && v_delta != 0 {
                let y = (self.offset_y + v_preset_row) % CDG_BUFFER_TILES_HIGH;
                for x in 0..CDG_BUFFER_TILES_WIDE {
                    self.buffer[y][x] = CdgTile::new(preset_color);
                }
            }

            self.fine_scroll_y = std::cmp::min(usize::from(v_cmd & 0xf), 11);
        }
    }

    fn pixels(&self) -> impl Iterator<Item = usize> {
        let pixels_wide = RENDERED_H_BORDER_PIXELS * 2 + RENDERED_H_RANGE.len() * 6;

        #[derive(Copy, Clone)]
        struct ScanState {
            row_block: usize,
            y: usize,
            col_block: usize,
            x: usize,
        }

        let top_border =
            std::iter::repeat_n(self.border_color, RENDERED_V_BORDER_PIXELS * pixels_wide);
        let rows = std::iter::successors(
            Some(ScanState {
                row_block: (self.offset_y + 1) % CDG_BUFFER_TILES_HIGH,
                y: self.fine_scroll_y,
                col_block: (self.offset_x + 1) % CDG_BUFFER_TILES_WIDE,
                x: self.fine_scroll_x,
            }),
            |state| {
                let mut y = state.y;
                let mut row_block = state.row_block;

                y += 1;
                if y == 12 {
                    y = 0;
                    row_block += 1;
                    if row_block == CDG_BUFFER_TILES_HIGH {
                        row_block = 0;
                    }
                }

                Some(ScanState {
                    row_block,
                    y,
                    ..*state
                })
            },
        )
        .take(RENDERED_V_RANGE.len() * 12);

        let pixels = rows.flat_map(|row_start_state| {
            let left_border = std::iter::repeat_n(self.border_color, RENDERED_H_BORDER_PIXELS);
            let cols = std::iter::successors(Some(row_start_state), |state| {
                let mut x = state.x;
                let mut col_block = state.col_block;

                x += 1;
                if x == 6 {
                    x = 0;
                    col_block += 1;
                    if col_block == CDG_BUFFER_TILES_WIDE {
                        col_block = 0;
                    }
                }

                Some(ScanState {
                    col_block,
                    x,
                    ..*state
                })
            })
            .take(RENDERED_H_RANGE.len() * 6)
            .map(|state| self.buffer[state.row_block][state.col_block].pixel(state.x, state.y));
            let right_border = std::iter::repeat_n(self.border_color, RENDERED_H_BORDER_PIXELS);

            left_border.chain(cols).chain(right_border)
        });
        let bottom_border =
            std::iter::repeat_n(self.border_color, RENDERED_V_BORDER_PIXELS * pixels_wide);

        top_border.chain(pixels).chain(bottom_border)
    }

    fn render_frame(&self) -> Vec<u8> {
        let pixels_wide = RENDERED_H_BORDER_PIXELS * 2 + RENDERED_H_RANGE.len() * 6;
        let pixels_high = RENDERED_V_BORDER_PIXELS * 2 + RENDERED_V_RANGE.len() * 12;

        let clut_rgb8: [[u8; 3]; CDG_CLUT_ENTRIES] = self.clut.map(CdgColor::to_rgb8_bytes);

        let mut image = Vec::with_capacity(pixels_wide * pixels_high * 3);
        for color in self.pixels() {
            image.extend(clut_rgb8[color])
        }

        image
    }
}

const CDG_BUFFER_TILE_ROWS: usize = 12;

#[derive(Clone, Copy, Default)]
struct CdgTile {
    planes: [[u8; CDG_BUFFER_TILE_ROWS]; 4],
}

impl CdgTile {
    fn new(color: u8) -> Self {
        Self {
            planes: array::from_fn(|plane| {
                [if color & (1 << plane) == 0 { 0 } else { 0x3f }; CDG_BUFFER_TILE_ROWS]
            }),
        }
    }

    fn color_row_for_plane(c0: u8, c1: u8, plane: usize, row_byte: u8) -> u8 {
        (if c0 & (1 << plane) == 0 {
            0
        } else {
            0x3f & !row_byte
        }) | (if c1 & (1 << plane) == 0 {
            0
        } else {
            0x3f & row_byte
        })
    }

    fn set_cmd(&mut self, c0: u8, c1: u8, cmd_bytes: &[u8; CDG_CMD_LEN]) {
        self.planes = array::from_fn(|plane| {
            array::from_fn(|row_i| Self::color_row_for_plane(c0, c1, plane, cmd_bytes[8 + row_i]))
        })
    }

    fn xor_cmd(&mut self, c0: u8, c1: u8, cmd_bytes: &[u8; CDG_CMD_LEN]) {
        self.planes = array::from_fn(|plane| {
            array::from_fn(|row_i| {
                self.planes[plane][row_i]
                    ^ Self::color_row_for_plane(c0, c1, plane, cmd_bytes[8 + row_i])
            })
        });
    }

    fn pixel(&self, x: usize, y: usize) -> usize {
        usize::from(
            ((self.planes[0][y] >> (5 - x)) & 1)
                | ((self.planes[1][y] >> (5 - x) << 1) & 2)
                | ((self.planes[2][y] >> (5 - x) << 2) & 4)
                | ((self.planes[3][y] >> (5 - x) << 3) & 8),
        )
    }
}

#[derive(Clone, Copy, Default, Debug)]
struct CdgColor {
    r: u8,
    g: u8,
    b: u8,
}

impl CdgColor {
    fn from_rgb4_bytes(bytes: &[u8; 2]) -> Self {
        let r = (bytes[0] & 0b11_1100) >> 2;
        let g1 = (bytes[0] & 0b00_0011) << 2;
        let g0 = (bytes[1] & 0b11_0000) >> 4;
        let b = bytes[1] & 0b00_1111;

        Self { r, g: g1 | g0, b }
    }

    fn from_rgb4_cmd_bytes(cmd_bytes: &[u8; CDG_CMD_LEN], i: usize) -> Self {
        assert!(i < 8);
        let offset = 4 + i * 2;
        let rgb_bytes: &[u8; 2] = (&cmd_bytes[offset..offset + 2]).try_into().unwrap();
        Self::from_rgb4_bytes(rgb_bytes)
    }

    fn to_rgb8_bytes(self) -> [u8; 3] {
        [(self.r << 4) | 8, (self.g << 4) | 8, (self.b << 4) | 8]
    }

    fn mix_to_rgb8_bytes(self, other: CdgColor) -> [u8; 3] {
        fn mix(c0: u8, c1: u8) -> u8 {
            (c0 + c1).saturating_mul(16) | 8
        }

        [
            mix(self.r, other.r),
            mix(self.g, other.g),
            mix(self.b, other.b),
        ]
    }
}

const CDEG_CLUT_ENTRIES: usize = 256;
struct CdegState {
    write_mode: CdegWriteMode,
    display_mode: CdegDisplayMode,

    primary: CdgState,
    secondary: CdgState,
    clut: [CdegColor; CDEG_CLUT_ENTRIES],
    _256_clut_rgb8: [[u8; 3]; CDEG_CLUT_ENTRIES],
}

impl CdegState {
    fn exec_item_1(&mut self, command: &[u8; CDG_CMD_LEN]) {
        if matches!(self.display_mode, CdegDisplayMode::_256Color) {
            match self.write_mode {
                CdegWriteMode::Both => {
                    // write enabled for both, mostly only applies to primary
                    match command[1] {
                        1 | 2 | 6 | 38 => {
                            // memory, border preset, set and xor font
                            self.primary.exec(command);
                        }
                        20 | 24 => {
                            // scroll
                            self.primary.exec(command);
                        }
                        30 | 31 => {
                            // item 1 clut
                            self.load_clut_cmd(command, usize::from(command[1] - 30) * 8);
                        }
                        _ => {
                            // others ignored
                        }
                    }

                    // secondary has special case for preset
                    match command[1] {
                        1 => {
                            self.secondary.buffer =
                                [[CdgTile::new(0); CDG_BUFFER_TILES_WIDE]; CDG_BUFFER_TILES_HIGH];
                            self.secondary.fine_scroll_x = 0;
                            self.secondary.fine_scroll_y = 0;
                        }
                        2 => {
                            self.secondary.border_color = 0;
                        }
                        20 | 24 => {
                            self.secondary.exec(command);
                        }
                        _ => {
                            // others ignored
                        }
                    }
                }

                CdegWriteMode::NoWrite => {
                    // Write disabled for CDG-only item 1
                }

                CdegWriteMode::Primary | CdegWriteMode::Secondary => {
                    panic!(
                        "write mode {:?} should be impossible for 256-color display mode",
                        self.write_mode
                    );
                }
            }
        } else {
            if matches!(
                self.write_mode,
                CdegWriteMode::Primary | CdegWriteMode::Both
            ) {
                self.primary.exec(command);
            }
            if matches!(
                self.write_mode,
                CdegWriteMode::Secondary | CdegWriteMode::Both
            ) {
                self.secondary.exec(command);
            }
        }
    }

    fn exec_item_2(&mut self, command: &[u8; CDG_CMD_LEN]) {
        if command[1] == 3 {
            self.memory_control_cmd(command);
            return;
        } else if matches!(self.display_mode, CdegDisplayMode::_256Color)
        /*&& matches!(self.write_mode, CdegWriteMode::Both)*/
        {
            // accept item 2 commands in 256 color mode
        } else if command[1] == 16 || command[1] == 17 {
            // accept first two load CLUT commands in all modes
        } else {
            // ignore item 2 commands in other modes
            return;
        }

        match command[1] {
            6 => self.secondary.font_cmd(command, false),
            14 => self.secondary.font_cmd(command, true),
            16..48 => self.load_clut_cmd(command, usize::from(command[1] - 16) * 8),
            48..64 => self.load_clut_2_cmd(command, usize::from(command[1] - 48) * 16),
            _ => {
                // unknown command, ignored
            }
        }
    }

    fn memory_control_cmd(&mut self, cmd_bytes: &[u8; CDG_CMD_LEN]) {
        // TODO check other copy?
        let mode = cmd_bytes[4];
        let write = mode & 3;
        let display = (mode >> 2) & 3;
        if display == 0 && (write == 1 || write == 2) {
            // no-op; 256 color mode only can use NoWrite or Both
            return;
        }

        self.write_mode = match write {
            0 => CdegWriteMode::NoWrite,
            1 => CdegWriteMode::Primary,
            2 => CdegWriteMode::Secondary,
            3 => CdegWriteMode::Both,
            _ => unreachable!(),
        };

        self.display_mode = match display {
            0 => CdegDisplayMode::_256Color,
            1 => CdegDisplayMode::Primary,
            2 => CdegDisplayMode::Secondary,
            3 => CdegDisplayMode::Mix,
            _ => unreachable!(),
        };

        if matches!(self.write_mode, CdegWriteMode::Both)
            && matches!(self.display_mode, CdegDisplayMode::_256Color)
        {
            // install the 256 color CLUT
            self._256_clut_rgb8 = self.clut.map(CdegColor::to_rgb8_bytes);
        }
    }

    fn load_clut_cmd(&mut self, cmd_bytes: &[u8; CDG_CMD_LEN], load_start_index: usize) {
        if !matches!(self.display_mode, CdegDisplayMode::_256Color) {
            // The first two item 2 load CLUT commands work directly on the
            // 16 color CLUTs in modes other than 256 color.
            // This allows dissolve to work without affecting CDG-only decoders.
            if matches!(
                self.write_mode,
                CdegWriteMode::Primary | CdegWriteMode::Both
            ) {
                self.primary.load_clut_cmd(cmd_bytes, load_start_index);
            }
            if matches!(
                self.write_mode,
                CdegWriteMode::Secondary | CdegWriteMode::Both
            ) {
                self.secondary.load_clut_cmd(cmd_bytes, load_start_index);
            }

            return;
        }

        for i in 0..8 {
            let clut_index = load_start_index + i;
            self.clut[clut_index].set_high(CdgColor::from_rgb4_cmd_bytes(cmd_bytes, i));
        }
    }

    fn load_clut_2_cmd(&mut self, cmd_bytes: &[u8; CDG_CMD_LEN], load_start_index: usize) {
        for i in 0..16 {
            let clut_index = load_start_index + i;
            self.clut[clut_index].set_low_from_cmd_bytes(cmd_bytes, i);
        }
    }

    /// accessed like `clut[primary_index][secondary_index]`
    fn mix_clut(&self) -> [[[u8; 3]; CDG_CLUT_ENTRIES]; CDG_CLUT_ENTRIES] {
        array::from_fn(|primary_i| {
            array::from_fn(|secondary_i| {
                self.primary.clut[primary_i].mix_to_rgb8_bytes(self.secondary.clut[secondary_i])
            })
        })
    }

    fn render_frame(&self) -> Vec<u8> {
        match self.display_mode {
            CdegDisplayMode::_256Color => self.render_frame_256_color(),
            CdegDisplayMode::Primary => self.primary.render_frame(),
            CdegDisplayMode::Secondary => self.secondary.render_frame(),
            CdegDisplayMode::Mix => self.render_frame_mix(),
        }
    }

    fn render_frame_mix(&self) -> Vec<u8> {
        let pixels_wide = RENDERED_H_BORDER_PIXELS * 2 + RENDERED_H_RANGE.len() * 6;
        let pixels_high = RENDERED_V_BORDER_PIXELS * 2 + RENDERED_V_RANGE.len() * 12;

        let clut_rgb8 = self.mix_clut();

        let mut image = Vec::with_capacity(pixels_wide * pixels_high * 3);

        for (primary_pixel, secondary_pixel) in self.primary.pixels().zip(self.secondary.pixels()) {
            image.extend(clut_rgb8[primary_pixel][secondary_pixel]);
        }

        image
    }

    fn render_frame_256_color(&self) -> Vec<u8> {
        let pixels_wide = RENDERED_H_BORDER_PIXELS * 2 + RENDERED_H_RANGE.len() * 6;
        let pixels_high = RENDERED_V_BORDER_PIXELS * 2 + RENDERED_V_RANGE.len() * 12;

        let mut image = Vec::with_capacity(pixels_wide * pixels_high * 3);

        for (primary_pixel, secondary_pixel) in self.primary.pixels().zip(self.secondary.pixels()) {
            image.extend(self._256_clut_rgb8[primary_pixel | (secondary_pixel << 4)]);
        }

        image
    }
}

impl Default for CdegState {
    fn default() -> Self {
        Self {
            write_mode: CdegWriteMode::Primary,
            display_mode: CdegDisplayMode::Primary,

            primary: Default::default(),
            secondary: Default::default(),
            clut: [Default::default(); CDEG_CLUT_ENTRIES],
            _256_clut_rgb8: [Default::default(); CDEG_CLUT_ENTRIES],
        }
    }
}

#[derive(Debug)]
enum CdegWriteMode {
    NoWrite,
    Primary,
    Secondary,
    Both,
}

#[derive(Debug)]
enum CdegDisplayMode {
    _256Color,
    Primary,
    Secondary,
    Mix,
}

#[derive(Clone, Copy, Default, Debug)]
struct CdegColor {
    r: u8,
    g: u8,
    b: u8,
}

impl CdegColor {
    fn set_high(&mut self, rgb4: CdgColor) {
        self.r = (self.r & 0b00_0011) | (rgb4.r << 2);
        self.g = (self.g & 0b00_0011) | (rgb4.g << 2);
        self.b = (self.b & 0b00_0011) | (rgb4.b << 2);
    }

    fn set_low_from_cmd_bytes(&mut self, cmd_bytes: &[u8; CDG_CMD_LEN], i: usize) {
        assert!(i < 16);
        let offset = 4 + i;
        let byte = cmd_bytes[offset];

        let r = (byte & 0b11_0000) >> 4;
        let g = (byte & 0b00_1100) >> 2;
        let b = byte & 0b00_0011;

        self.r = (self.r & 0b11_1100) | r;
        self.g = (self.g & 0b11_1100) | g;
        self.b = (self.b & 0b11_1100) | b;
    }

    fn to_rgb8_bytes(self) -> [u8; 3] {
        //[(self.r << 2) | 2, (self.g << 2) | 2, (self.b << 2) | 2]
        [self.r << 2, self.g << 2, self.b << 2]
    }
}

fn decode_cdg(child_stdin: &mut ChildStdin, commands: &[u8], cdeg_enabled: bool) {
    let mut cdeg_state = CdegState::default();

    for (i, command) in commands.chunks(CDG_CMD_LEN).enumerate() {
        // TODO before? after? mid?
        if i % 5 == 0 {
            let image_data = if cdeg_enabled {
                cdeg_state.render_frame()
            } else {
                cdeg_state.primary.render_frame()
            };
            child_stdin.write_all(&image_data).expect("write frame");
        }

        let command: &[u8; CDG_CMD_LEN] = command.try_into().unwrap();
        const CDG_ITEM_1: u8 = 8 | 1;
        const CDG_ITEM_2: u8 = 8 | 2;
        if command[0] == CDG_ITEM_1 {
            cdeg_state.exec_item_1(command);
        } else if command[0] == CDG_ITEM_2 {
            if cdeg_enabled {
                cdeg_state.exec_item_2(command);
            }
        } else {
            // other modes ignored
        }
    }

    child_stdin.flush().expect("flush frames");
}
