#!/usr/bin/env python3

import argparse
import logging
import os
import subprocess
import sys
import tempfile
from typing import List, Optional, TextIO


class Logger:
    def __init__(self, verbosity: int, no_log_term: bool, log_file: Optional[str], log_json: Optional[str]):
        self.verbosity = verbosity
        self.no_log_term = no_log_term
        self.log_file = log_file
        self.log_json = log_json

    @property
    def logging_level(self) -> int:
        assert self.verbosity >= 0
        if self.verbosity == 0:
            return logging.ERROR
        if self.verbosity == 1:
            return logging.WARNING
        if self.verbosity == 2:
            return logging.INFO
        return logging.DEBUG

    @staticmethod
    def term_formatter() -> logging.Formatter:
        log_fmt = logging.Formatter('[{asctime} {levelname:5.5} {module}] {message}',
                                    style='{')
        log_fmt.datefmt = '%H:%M:%S'
        return log_fmt

    @staticmethod
    def file_formatter() -> logging.Formatter:
        log_fmt = logging.Formatter('[{asctime} {levelname:5.5} {module}] {message}',
                                    style='{')
        log_fmt.datefmt = '%H:%M:%S'
        return log_fmt

    @staticmethod
    def json_formatter() -> logging.Formatter:
        format = '{{"time":"{asctime}","message":"{message}","module_path":"{module}","file":"{filename}","line":{lineno},"level":"{levelname}","target":"{module}"}}'
        log_fmt = logging.Formatter(format, style='{')
        return log_fmt

    @staticmethod
    def get_term_handler(no_log_term: bool) -> 'logging.StreamHandler[TextIO]':
        term_handler = logging.StreamHandler(sys.stderr)
        if no_log_term:
            term_handler.setLevel(logging.ERROR)
        term_handler.setFormatter(Logger.term_formatter())
        return term_handler

    @staticmethod
    def get_file_handler(log_file: str, formatter: logging.Formatter) -> logging.FileHandler:
        file_handler = logging.FileHandler(log_file, encoding='utf-8')
        file_handler.setFormatter(formatter)
        return file_handler

    def setup_logging(self):
        logger = logging.getLogger()
        logger.setLevel(self.logging_level)

        logger.addHandler(Logger.get_term_handler(self.no_log_term))
        if self.log_file is not None:
            logger.addHandler(Logger.get_file_handler(
                self.log_file, Logger.file_formatter()))
        if self.log_json is not None:
            logger.addHandler(Logger.get_file_handler(
                self.log_json, Logger.json_formatter()))

    def cmd_args(self) -> List[str]:
        args: List[str] = []
        if self.verbosity > 0:
            args.append(f"-{'v' * self.verbosity}")
        if not self.no_log_term:
            args.append('--no-log-term')
        if self.log_file is not None:
            args.extend(['--log-file', self.log_file])
        if self.log_json is not None:
            args.extend(['--log-json', self.log_json])
        return args


def convert(input_world_file: str, output_world_file: str, input_format: str, output_format: str):
    try:
        cmd = ['rofi-convert', input_world_file, output_world_file,
               '--input-format', input_format,
               '--output-format', output_format,
               ]
        logging.debug(f'Running cmd: {subprocess.list2cmdline(cmd)}')
        subprocess.check_call(cmd, stdout=sys.stderr)
    except subprocess.CalledProcessError as _:
        logging.fatal(
            f"Error while converting '{input_world_file}' (format: {input_format}) to '{output_world_file}' (format: {output_format})")
        exit(1)


def voxel_reconfig(init_voxel_file: str, goal_voxel_file: str, args: List[str], output: TextIO, logger: Logger):
    try:
        logging.info('Starting voxel reconfiguration')

        cmd = ['rofi-voxel_main', init_voxel_file, goal_voxel_file] \
            + args + logger.cmd_args()

        logging.debug(f'Running cmd: {subprocess.list2cmdline(cmd)}')
        subprocess.check_call(cmd, stdout=output)

        logging.info('Ended voxel reconfiguration')
    except subprocess.CalledProcessError as _:
        logging.fatal('Error while running voxel reconfig')
        exit(1)


def convert_and_reconfig(init_world_file: str, goal_world_file: str, init_format: str, goal_format: str, args: List[str], output: TextIO, logger: Logger):
    assert init_world_file != '-' or goal_world_file != '-'

    with tempfile.TemporaryDirectory() as tmp_dir:
        if init_format == 'voxel':
            init_voxel_file = init_world_file
        else:
            init_voxel_file = tempfile.NamedTemporaryFile(suffix='.json',
                                                          dir=tmp_dir).name
            convert(init_world_file, init_voxel_file,
                    input_format=init_format, output_format='voxel')

        if goal_format == 'voxel':
            goal_voxel_file = goal_world_file
        else:
            goal_voxel_file = tempfile.NamedTemporaryFile(suffix='.json',
                                                          dir=tmp_dir).name
            convert(goal_world_file, goal_voxel_file,
                    input_format=goal_format, output_format='voxel')

        voxel_reconfig(init_voxel_file, goal_voxel_file, args=args,
                       output=output, logger=logger)


def check_input_files(parser: argparse.ArgumentParser, files: List[str]):
    if files.count('-') > 1:
        parser.error("multiple input file arguments have value '-' (stdin)")

    for file in files:
        if file == '-':
            return
        if not os.path.exists(file):
            parser.error(f"can't open '{file}': No such file or directory")
        if not os.path.isfile(file):
            parser.error(f"can't open '{file}': Is a directory")
        if not os.access(file, os.R_OK):
            parser.error(f"can't open '{file}': Is not readable")


if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description='Run voxel reconfig. Returns the reconfig sequence in voxel format to stdout.')
    parser.add_argument('init_world_file', type=str,
                        help="Path to the init world file ('-' for stdin)")
    parser.add_argument('goal_world_file', type=str,
                        help="Path to the goal world file ('-' for stdin)")
    parser.add_argument('--output', '-o', type=argparse.FileType('w'),
                        help="Path to the output file (default or '-' for stdout)")

    parser.add_argument('--format', '-f', type=str, default='json',
                        choices=['json', 'voxel', 'old'],
                        help='Format of world files (default: %(default)s)')
    parser.add_argument('--init-format', '--if', type=str,
                        choices=['json', 'voxel', 'old'],
                        help='Format of the init world file (overrides --format)')
    parser.add_argument('--goal-format', '--gf', type=str,
                        choices=['json', 'voxel', 'old'],
                        help='Format of the goal world file (overrides --format)')

    parser.add_argument('--repr', '-r', type=str, default='map',
                        choices=['map', 'matrix', 'sortvec'],
                        help='Voxel world representation for reconfig algorithm (default: %(default)s)')
    parser.add_argument('--alg', '-a', type=str, default='bfs',
                        choices=['bfs',
                                 'astar-zero', 'astar-zero-nopt',
                                 'astar-naive', 'astar-naive-nopt',
                                 ],
                        help='Algorithm for reconfiguration (default: %(default)s)')

    log_parser = parser.add_argument_group('logging')
    log_parser.add_argument('--verbose', '-v', action='count', default=0,
                            help='Increase verbosity level')
    log_parser.add_argument('--no-log-term', action='store_true',
                            help="Don't log to terminal (Errors will still be logged)")
    log_parser.add_argument('--log-file', type=str,
                            help='Log to file in human readable format')
    log_parser.add_argument('--log-json', type=str,
                            help='Log to file in json format (each log as one json)')

    args = parser.parse_args()
    check_input_files(parser, [args.init_world_file, args.goal_world_file])
    logger = Logger(verbosity=args.verbose, no_log_term=args.no_log_term,
                    log_file=args.log_file, log_json=args.log_json)
    logger.setup_logging()

    convert_and_reconfig(args.init_world_file, args.goal_world_file,
                         args.format if args.init_format is None else args.init_format,
                         args.format if args.goal_format is None else args.goal_format,
                         args=['--repr', args.repr, '--alg', args.alg],
                         output=args.output, logger=logger)
