from PyCriCodecs import ACB
import sys
from abc import abstractmethod
from pathlib import Path
import argparse
import subprocess
import yaml
import shutil
import string
import random
import struct
from collections import defaultdict


class Extracter:
    def __init__(self, *, config, out_dir, no_overwrite, no_music, no_audio, no_jackets, no_images, **kwargs):
        with open(config, 'r') as f:
            self.config = yaml.safe_load(f)
        self.music_enabled = self.config['music']['enable']
        self.jackets_enabled = self.config['jackets']['enable']
        self.images_enabled = self.config['images']['enable']
        self.audio_enabled = self.config['audio']['enable']
        self.out_dir = Path(out_dir)
        self.tmp_dir = self.out_dir / 'tmp'
        self.no_overwrite = no_overwrite
        self.no_music = no_music
        self.no_audio = no_audio
        self.no_jackets = no_jackets
        self.no_images = no_images

    def get_tmp(self, ext='.dat'):
        self.tmp_dir.mkdir(parents=True, exist_ok=True)
        while True:
            name = ''.join(random.choices(string.ascii_letters + string.digits + '_-+', k=32))
            path = self.tmp_dir / (name + ext)
            if not path.exists():
                try:
                    path.touch(exist_ok=False)
                    return path
                except FileExistsError:
                    pass

    def vgmstream(self, input_file):
        is_tmp = False
        if type(input_file) == bytes:
            tmp = self.get_tmp('.hca')
            is_tmp = True
            with open(tmp, 'wb') as f:
                f.write(input_file)
            input_file = tmp

        args = [
            self.config['vgmstream_path'],
            '-p',
            input_file
        ]

        res = subprocess.run(args, capture_output=True)

        if res.returncode:
            sys.stderr.buffer.write(res.stderr)
            raise RuntimeError(f'vgmstream exited with code {res.returncode}')

        if is_tmp:
            input_file.unlink()

        return res.stdout

    def ffmpeg(self, input_args, media_type, output_name):
        buffer_input = None
        input_args = list(input_args)
        out_path = Path(output_name).with_suffix(self.config[media_type]['extension'])
        if self.no_overwrite and out_path.exists():
            return

        for i, arg in enumerate(input_args):
            if type(arg) == bytes:
                if buffer_input is not None:
                    raise ValueError('more than one buffer passed to ffmpeg input')
                buffer_input = arg
                input_args[i] = '-'
            else:
                input_args[i] = str(arg)

        args = [
            self.config['ffmpeg_path'],
            ('-n' if self.no_overwrite else '-y'),
            '-hide_banner',
            '-loglevel',
            'error',
            *input_args,
            *self.config[media_type].get('ffmpeg_args', []),
            out_path
        ]

        if buffer_input:
            res = subprocess.run(args, capture_output=True, input=buffer_input)
        else:
            res = subprocess.run(args, capture_output=True)

        if res.returncode:
            sys.stderr.buffer.write(res.stderr)
            raise RuntimeError(f'ffmpeg exited with code {res.returncode}')

    def cleanup(self):
        shutil.rmtree(self.tmp_dir, ignore_errors=True)

    @staticmethod
    def acb_filenames(acb: ACB):
        awb_dict: dict[int, dict[int, tuple[int, str]]] = defaultdict(dict)

        payload = acb.payload[0]
        for name_entry in payload['CueNameTable']:
            name = name_entry['CueName'][1]
            index = name_entry['CueIndex'][1]

            sequence = payload['SequenceTable'][index]
            num_tracks = sequence['NumTracks'][1]
            if not num_tracks:
                continue

            track_indexes = struct.unpack(f'>{num_tracks}H', sequence['TrackIndex'][1])
            waveforms = []
            for track_index in track_indexes:
                waveform_index = int.from_bytes(payload['SynthTable'][track_index]['ReferenceItems'][1][2:], 'big')
                waveforms.append(payload['WaveformTable'][waveform_index])

            for i, waveform in enumerate(waveforms):
                awb_index = waveform['StreamAwbPortNo'][1]
                stream_index = waveform['StreamAwbId'][1]
                awb_dict[awb_index][stream_index] = i, name

        return awb_dict

    @abstractmethod
    def extract_jacket(self):
        raise NotImplementedError

    @abstractmethod
    def extract_images(self):
        raise NotImplementedError

    @abstractmethod
    def extract_music(self):
        raise NotImplementedError

    @abstractmethod
    def extract_audio(self):
        raise NotImplementedError

    def extract(self):
        if self.jackets_enabled and not self.no_jackets:
            yield from self.extract_jacket()
        if self.images_enabled and not self.no_images:
            yield from self.extract_images()
        if self.music_enabled and not self.no_music:
            yield from self.extract_music()
        if self.audio_enabled and not self.no_audio:
            yield from self.extract_audio()

    @staticmethod
    @abstractmethod
    def register(parser: argparse.ArgumentParser):
        raise NotImplementedError


extracters: dict[str, Extracter] = {}


def add_extracter(extracter):
    extracters[extracter.__name__.lower()] = extracter


def get_extracters():
    return extracters
