import logging
import pathlib
import re

import yaml

from ..asset import font, image, map, raw
from ..core.outputformat import CHeader, CSource, RawBinary
from ..core.palette import Palette
from ..core.tool import Tool


class Packer(Tool):
    command = 'pack'
    help = 'Pack a collection of assets for 32Blit'

    def __init__(self, parser):
        Tool.__init__(self, parser)
        self.parser.add_argument('--config', type=pathlib.Path, help='Asset config file')
        self.parser.add_argument('--output', type=pathlib.Path, help='Name for output file(s) or root path when using --config')
        self.parser.add_argument('--files', nargs='+', type=pathlib.Path, help='Input files')
        self.parser.add_argument('--force', action='store_true', help='Force file overwrite')

        self.config = {}
        self.assets = []
        self.general_options = {}

    def parse_config(self, config_file):
        config = open(config_file).read()
        config = yaml.safe_load(config)

        self.config = config

    def filelist_to_config(self, filelist, output):
        config = {}

        for file in filelist:
            config[file] = {}

        self.config = {f'{output}': config}

    def get_general_options(self):
        for key, value in self.config.items():
            if key in ():
                self.general_options[key] = value

        for key, value in self.general_options.items():
            self.config.items.pop(key)

    def run(self, args):
        self.working_path = pathlib.Path('.')

        if args.config is not None:
            if args.config.is_file():
                self.working_path = args.config.parent
            else:
                logging.warning(f'Unable to find config at {args.config}')

        if args.output is not None:
            self.destination_path = args.output
        else:
            self.destination_path = self.working_path

        if args.config is not None:
            self.parse_config(args.config)
            logging.info(f'Using config at {args.config}')

        elif args.files is not None and args.output is not None:
            self.filelist_to_config(args.files, args.output)

        self.get_general_options()

        # Top level of our config is filegroups and general settings
        for target, options in self.config.items():
            output_file = self.working_path / target
            logging.info(f'Preparing output target {output_file}')

            asset_sources = []
            target_options = {}

            for key, value in options.items():
                if key in AssetTarget.supported_options:
                    target_options[key] = value

            # Strip high-level options from the dict
            # Leaving just file source globs
            for key, value in target_options.items():
                options.pop(key)

            for file_glob, file_options in options.items():
                # Treat the input string as a glob, and get an input filelist
                if type(file_glob) is str:
                    input_files = list(self.working_path.glob(file_glob))
                else:
                    input_files = [file_glob]
                if len(input_files) == 0:
                    logging.warning(f'Input file(s) not found {self.working_path / file_glob}')
                    continue

                # Rewrite a single string option to `name: option`
                # This handles: `inputfile.bin: filename` entries
                if type(file_options) is str:
                    file_options = {'name': file_options}

                elif file_options is None:
                    file_options = {}

                asset_sources.append(
                    AssetSource(input_files, file_options=file_options, working_path=self.working_path)
                )

            self.assets.append(AssetTarget(
                output_file,
                asset_sources,
                target_options
            ))

        self.destination_path.mkdir(parents=True, exist_ok=True)

        for target in self.assets:
            output = target.build()
            self.output(output, self.destination_path / target.output_file.name, target.output_format, force=args.force)


class AssetSource():
    supported_options = ('name', 'type')

    def __init__(self, input_files, file_options, working_path):
        self.asset_builders = {
            image.ImageAsset.command: image.ImageAsset,
            font.FontAsset.command: font.FontAsset,
            map.MapAsset.command: map.MapAsset,
            raw.RawAsset.command: raw.RawAsset
        }

        self.input_files = input_files
        self.working_path = working_path
        self.builder_options = {}
        self.type = None
        self.name = None
        self.parse_source_options(file_options)

    def parse_source_options(self, opts):
        for option, value in opts.items():
            if option not in self.supported_options:
                # These options are passed into the Map/Image/Raw builder
                self.builder_options[option] = value
            else:
                setattr(self, option, value)

        if self.type is None:
            # Glob files all have the same suffix, so we only care about the first one
            self.type = self.guess_type(self.input_files[0])

    def guess_type(self, file):
        # We need a high-level guess type that can query the types each builder supports
        # and dispatch our builds to the right one
        for command, asset_builder in self.asset_builders.items():
            for input_type, suffixes in asset_builder.typemap.items():
                if file.suffix in suffixes:
                    return f'{command}/{input_type}'
        logging.warning(f'Unable to guess type, assuming raw/binary {file}')
        return 'raw/binary'

    def build(self, format, prefix=None, output_file=None):
        input_type, input_subtype = self.type.split('/')
        builder = self.asset_builders[input_type]()

        # Now we know our target builder, one last iteration through the options
        # allows some pre-processing stages to remap paths or other idiosyncrasies
        # of the yml config format.
        for option_name, option_value in builder.options.items():
            if option_name in self.builder_options:
                option_type = builder.options[option_name]
                if type(option_type) is tuple:
                    option_type, default_value = option_type

                # Ensure Palette and pathlib.Path type options for this asset builder
                # are prefixed with the working directory if they are not absolute paths
                if option_type in (Palette, pathlib.Path):
                    if not pathlib.Path(self.builder_options[option_name]).is_absolute():
                        self.builder_options[option_name] = self.working_path / self.builder_options[option_name]

        output = []

        for file in self.input_files:
            symbol_name = self.name
            if symbol_name is not None:
                symbol_name = symbol_name.format(
                    filename=file.with_suffix('').name,
                    filepath=file.with_suffix(''),
                    fullname=file.name,
                    fullpath=file,
                    type=input_type,
                    subtype=input_subtype
                )
                symbol_name = symbol_name.replace('.', '_')
                symbol_name = re.sub('[^0-9A-Za-z_]', '_', symbol_name)

            self.builder_options.update({
                'input_file': file,
                'output_file': output_file,
                'input_type': input_subtype,
                'output_format': format,
                'symbol_name': symbol_name,
                'working_path': self.working_path,
                'prefix': prefix
            })
            builder.prepare_options(self.builder_options)
            output.append(builder.build())
            logging.info(f' - {self.type} {file} -> {builder.symbol_name}')

        return output


class AssetTarget():
    supported_options = ('prefix', 'type')
    output_formats = {
        '.hpp': CHeader,
        '.cpp': CSource,
        '.blit': RawBinary,
        '.raw': RawBinary
    }

    def __init__(self, output_file, sources, target_options):
        self.output_file = output_file
        self.sources = sources
        self.parse_target_options(target_options)

    def parse_target_options(self, target_options):
        self.prefix = target_options.get('prefix')
        output_format = target_options.get('type')
        if output_format is None:
            output_format = self.guess_output_format(self.output_file)
        else:
            if type not in self.output_formats.values():
                raise ValueError(f'Unknown asset output format {output_format}')
        self.output_format = output_format

    def build(self):
        # TODO I hate this code, but because we're dealing with multiple input files
        # the output can potentially be spread across multiple files with multiple snippets
        # We need to gather each input files declarations/definitions (or otherwise) into
        # the right output file.
        output = []

        # If the output_format deals with multiple files, switch to a dict and prep the files
        if self.output_format.components is not None:
            output = {}
            for ext in self.output_format.components:
                output[ext] = []

        # Iterate through all the sources (input files) and build them
        for source in self.sources:
            data = source.build(format=self.output_format, output_file=self.output_file, prefix=self.prefix)
            for snippet in data:
                if self.output_format.components is not None:
                    # If there are multiple output files, collect all the snippets into their respective outputs
                    for ext, content in snippet.items():
                        output[ext].append(content)
                else:
                    # If it's a single file format just append the snippet to the list
                    output.append(snippet)

        return output

    def guess_output_format(self, file):
        if file.suffix in self.output_formats:
            return self.output_formats[file.suffix]
        logging.warning(f'Unable to guess type of {file}, assuming raw/binary')
        return RawBinary
