#  Copyright (c) 2021-2024, Manfred Moitzi
#  License: MIT License
from __future__ import annotations

import pathlib
import tempfile
from typing import Callable, Optional, TYPE_CHECKING, Type, Sequence
import abc
import sys
import os
import glob
import signal
import time
import logging
from pathlib import Path

# --------------------------------------------------------------------------------------
# Only imports from the core package here - no add-ons!
#
#   The command `ezdxf -V` must always work!
#
# Imports depending on additional packages like Pillow, Matplotlib, PySide6, ...
# have to be local imports, see 'draw' command as an example.
# --------------------------------------------------------------------------------------
import ezdxf
from ezdxf import recover
from ezdxf.lldxf import const
from ezdxf.lldxf.validator import is_dxf_file, is_binary_dxf_file, dxf_info
from ezdxf.dwginfo import dwg_file_info

if TYPE_CHECKING:
    from ezdxf.entities import DXFGraphic
    from ezdxf.addons.drawing.properties import Properties, LayerProperties

__all__ = ["get", "add_parsers"]

logger = logging.getLogger("ezdxf")


def get(cmd: str) -> Optional[Callable]:
    cls = _commands.get(cmd)
    if cls:
        return cls.run
    return None


def add_parsers(subparsers) -> None:
    for cmd in _commands.values():  # in order of registration
        try:
            cmd.add_parser(subparsers)
        except ImportError:
            logger.info(f"ImportError - '{cmd.NAME}' command not available")


def is_dxf_r12_file(filename: str) -> bool:
    try:
        with open(filename, "rt", errors="ignore") as fp:
            info = dxf_info(fp)
    except IOError:
        return False
    return info.version <= const.DXF12


class Command:
    """abstract base class for launcher commands"""

    NAME = "command"

    @staticmethod
    @abc.abstractmethod
    def add_parser(subparsers) -> None:
        pass

    @staticmethod
    @abc.abstractmethod
    def run(args) -> None:
        pass


_commands: dict[str, Type[Command]] = dict()


def register(cls: Type[Command]):
    """Register a launcher sub-command."""
    _commands[cls.NAME] = cls
    return cls


@register
class Audit(Command):
    """Launcher sub-command: audit"""

    NAME = "audit"

    @staticmethod
    def add_parser(subparsers):
        parser = subparsers.add_parser(Audit.NAME, help="audit and repair DXF files")
        parser.add_argument(
            "files",
            metavar="FILE",
            nargs="+",
            help="audit DXF files",
        )
        parser.add_argument(
            "-s",
            "--save",
            action="store_true",
            help='save recovered files with extension ".rec.dxf" ',
        )
        parser.add_argument(
            "-x",
            "--explore",
            action="store_true",
            help="filters invalid DXF tags, this may load corrupted files but "
            "data loss is very likely",
        )

    @staticmethod
    def run(args):
        def build_outname(name: str) -> str:
            p = Path(name)
            return str(p.parent / (p.stem + ".rec.dxf"))

        def log_fixes(auditor):
            for error in auditor.fixes:
                logger.info("fixed:" + error.message)

        def log_errors(auditor):
            for error in auditor.errors:
                logger.error(error.message)

        def _audit(filename: str) -> None:
            msg = f"auditing file: {filename}"
            print(msg)
            logger.info(msg)
            if args.explore:
                logger.info("explore mode - skipping invalid tags")
            loader = recover.explore if args.explore else recover.readfile
            try:
                doc, auditor = loader(filename)
            except IOError:
                msg = "Not a DXF file or a generic I/O error."
                print(msg)
                logger.error(msg)
                return  # keep on processing additional files
            except const.DXFStructureError as e:
                msg = f"Invalid or corrupted DXF file: {str(e)}"
                print(msg)
                logger.error(msg)
                return  # keep on processing additional files

            if auditor.has_errors:
                auditor.print_error_report()
                log_errors(auditor)
            if auditor.has_fixes:
                auditor.print_fixed_errors()
                log_fixes(auditor)

            if auditor.has_errors is False and auditor.has_fixes is False:
                print("No errors found.")
            else:
                print(
                    f"Found {len(auditor.errors)} errors, "
                    f"applied {len(auditor.fixes)} fixes"
                )

            if args.save:
                outname = build_outname(filename)
                try:
                    doc.saveas(outname)
                except IOError as e:
                    print(f"Can not save recovered file '{outname}':\n{str(e)}")
                else:
                    print(f"Saved recovered file as: '{outname}'")

        for pattern in args.files:
            names = list(glob.glob(pattern))
            if len(names) == 0:
                msg = f"File(s) '{pattern}' not found."
                print(msg)
                logger.error(msg)
                continue
            for filename in names:
                if not os.path.exists(filename):
                    msg = f"File '{filename}' not found."
                    print(msg)
                    logger.error(msg)
                    continue
                if not is_dxf_file(filename):
                    msg = f"File '{filename}' is not a DXF file."
                    print(msg)
                    logger.error(msg)
                    continue
                _audit(filename)


def load_document(filename: str):
    try:
        doc, auditor = recover.readfile(filename)
    except IOError:
        msg = f'Not a DXF file or a generic I/O error: "{filename}"'
        print(msg, file=sys.stderr)
        sys.exit(2)
    except const.DXFStructureError:
        msg = f'Invalid or corrupted DXF file: "{filename}"'
        print(msg, file=sys.stderr)
        sys.exit(3)

    if auditor.has_errors:
        # But is most likely good enough for rendering.
        msg = f"Audit process found {len(auditor.errors)} unrecoverable error(s)."
        print(msg)
        logger.error(msg)
    if auditor.has_fixes:
        msg = f"Audit process fixed {len(auditor.fixes)} error(s)."
        print(msg)
        logger.info(msg)
    return doc, auditor


HELP_LTYPE = (
    "select the line type rendering method, default is approximate. "
    "Approximate uses the closest approximation available to the "
    "backend, the accurate method renders as accurately as possible "
    "but this approach is slower."
)
HELP_LWSCALE = (
    "set custom line weight scaling, default is 0 to disable line " "weights at all"
)


@register
class Draw(Command):
    """Launcher sub-command: draw"""

    NAME = "draw"

    @staticmethod
    def add_parser(subparsers):
        parser = subparsers.add_parser(
            Draw.NAME, help="draw and save DXF as a bitmap or vector image"
        )
        parser.add_argument(
            "file",
            metavar="FILE",
            nargs="?",
            help="DXF file to view or convert",
        )
        parser.add_argument(
            "--backend",
            default="matplotlib",
            choices=["matplotlib", "qt", "mupdf", "custom_svg"],
            help="choose the backend to use for rendering",
        )
        parser.add_argument(
            "--formats",
            action="store_true",
            help="show all supported export formats and exit",
        )
        parser.add_argument(
            "-l",
            "--layout",
            default="Model",
            help='select the layout to draw, default is "Model"',
        )
        parser.add_argument(
            "--background",
            default="DEFAULT",
            choices=[
                "DEFAULT",
                "WHITE",
                "BLACK",
                "PAPERSPACE",
                "MODELSPACE",
                "OFF",
                "CUSTOM",
            ],
            help="choose the background color to use",
        )
        parser.add_argument(
            "--all-layers-visible",
            action="store_true",
            help="draw all layers including the ones marked as invisible",
        )
        parser.add_argument(
            "--all-entities-visible",
            action="store_true",
            help="draw all entities including the ones marked as invisible "
            "(some entities are individually marked as invisible even "
            "if the layer is visible)",
        )
        parser.add_argument(
            "-o",
            "--out",
            required=False,
            type=pathlib.Path,
            help="output filename for export",
        )
        parser.add_argument(
            "--dpi",
            type=int,
            default=300,
            help="target render resolution, default is 300",
        )
        parser.add_argument(
            "-f",
            "--force",
            action="store_true",
            help="overwrite the destination if it already exists",
        )
        parser.add_argument(
            "-v",
            "--verbose",
            action="store_true",
            help="give more output",
        )

    @staticmethod
    def run(args):
        try:
            from ezdxf.addons.drawing import RenderContext, Frontend
            from ezdxf.addons.drawing.config import Configuration, BackgroundPolicy
            from ezdxf.addons.drawing.file_output import (
                open_file,
                MatplotlibFileOutput,
                PyQtFileOutput,
                SvgFileOutput,
                MuPDFFileOutput,
            )
        except ImportError as e:
            print(str(e))
            sys.exit(1)

        if args.backend == "matplotlib":
            try:
                file_output = MatplotlibFileOutput(args.dpi)
            except ImportError as e:
                print(str(e))
                sys.exit(1)
        elif args.backend == "qt":
            try:
                file_output = PyQtFileOutput(args.dpi)
            except ImportError as e:
                print(str(e))
                sys.exit(1)
        elif args.backend == "mupdf":
            try:
                file_output = MuPDFFileOutput(args.dpi)
            except ImportError as e:
                print(str(e))
                sys.exit(1)
        elif args.backend == "custom_svg":
            # has no additional dependencies
            file_output = SvgFileOutput(args.dpi)
        else:
            raise ValueError(args.backend)

        verbose = args.verbose
        if verbose:
            logging.basicConfig(level=logging.INFO)

        if args.formats:
            print(f"formats supported by {args.backend}:")
            for extension, description in file_output.supported_formats():
                print(f"  {extension}: {description}")
            sys.exit(0)

        if args.file:
            filename = args.file
        else:
            print("argument FILE is required")
            sys.exit(1)

        print(f'loading file "{filename}"...')
        doc, _ = load_document(filename)

        try:
            layout = doc.layouts.get(args.layout)
        except KeyError:
            print(
                f'Could not find layout "{args.layout}". '
                f"Valid layouts: {[l.name for l in doc.layouts]}"
            )
            sys.exit(1)

        ctx = RenderContext(doc)
        config = Configuration().with_changes(
            background_policy=BackgroundPolicy[args.background]
        )
        out = file_output.backend()

        if args.all_layers_visible:
            def override_layer_properties(layer_properties: Sequence[LayerProperties]) -> None:
                for properties in layer_properties:
                    properties.is_visible = True

            ctx.set_layer_properties_override(override_layer_properties)

        frontend = Frontend(ctx, out, config=config)

        if args.all_entities_visible:
            def override_entity_properties(entity: DXFGraphic, properties: Properties) -> None:
                properties.is_visible = True
            frontend.push_property_override_function(override_entity_properties)

        t0 = time.perf_counter()
        if verbose:
            print(f"drawing layout '{layout.name}' ...")
        frontend.draw_layout(layout, finalize=True)
        t1 = time.perf_counter()
        if verbose:
            print(f"took {t1-t0:.4f} seconds")

        if args.out is not None:
            if pathlib.Path(args.out).suffix not in {
                f".{ext}" for ext, _ in file_output.supported_formats()
            }:
                print(
                    f'the format of the output path "{args.out}" '
                    f"is not supported by the backend {args.backend}"
                )
                sys.exit(1)

            if args.out.exists() and not args.force:
                print(f'the destination "{args.out}" already exists. Not writing')
                sys.exit(1)
            else:
                print(f'exporting to "{args.out}"...')
                t0 = time.perf_counter()
                file_output.save(args.out)
                t1 = time.perf_counter()
                if verbose:
                    print(f"took {t1 - t0:.4f} seconds")

        else:
            print(f"exporting to temporary file...")
            output_dir = pathlib.Path(tempfile.mkdtemp(prefix="ezdxf_draw"))
            output_path = output_dir / f"output.{file_output.default_format()}"
            file_output.save(output_path)
            print(f'saved to "{output_path}"')
            if verbose:
                print("opening viewer...")
            open_file(output_path)


@register
class View(Command):
    """Launcher sub-command: view"""

    NAME = "view"

    @staticmethod
    def add_parser(subparsers):
        parser = subparsers.add_parser(
            View.NAME, help="view DXF files by the PyQt viewer"
        )
        parser.add_argument(
            "file",
            metavar="FILE",
            nargs="?",
            help="DXF file to view",
        )
        parser.add_argument(
            "-l",
            "--layout",
            default="Model",
            help='select the layout to draw, default is "Model"',
        )
        # disable lineweight at all by default:
        parser.add_argument(
            "--lwscale",
            type=float,
            default=0,
            help=HELP_LWSCALE,
        )

    @staticmethod
    def run(args):
        # Import on demand for a quicker startup:
        try:
            from ezdxf.addons.xqt import QtWidgets
        except ImportError as e:
            print(str(e))
            sys.exit(1)
        from ezdxf.addons.drawing.qtviewer import CADViewer
        from ezdxf.addons.drawing.config import Configuration

        config = Configuration(
            lineweight_scaling=args.lwscale,
        )

        signal.signal(signal.SIGINT, signal.SIG_DFL)  # handle Ctrl+C properly
        app = QtWidgets.QApplication(sys.argv)
        app.setStyle("Fusion")
        set_app_icon(app)
        viewer = CADViewer.from_config(config)
        filename = args.file
        if filename:
            doc, auditor = load_document(filename)
            viewer.set_document(
                doc,
                auditor,
                layout=args.layout,
            )
        sys.exit(app.exec())


@register
class Browse(Command):
    """Launcher sub-command: browse"""

    NAME = "browse"

    @staticmethod
    def add_parser(subparsers):
        parser = subparsers.add_parser(Browse.NAME, help="browse DXF file structure")
        parser.add_argument(
            "file",
            metavar="FILE",
            nargs="?",
            help="DXF file to browse",
        )
        parser.add_argument(
            "-l", "--line", type=int, required=False, help="go to line number"
        )
        parser.add_argument(
            "-g",
            "--handle",
            required=False,
            help="go to entity by HANDLE, HANDLE has to be a hex value without "
            "any prefix like 'fefe'",
        )

    @staticmethod
    def run(args):
        try:
            from ezdxf.addons.xqt import QtWidgets
        except ImportError as e:
            print(str(e))
            sys.exit(1)
        from ezdxf.addons import browser

        signal.signal(signal.SIGINT, signal.SIG_DFL)  # handle Ctrl+C properly
        app = QtWidgets.QApplication(sys.argv)
        app.setStyle("Fusion")
        set_app_icon(app)
        main_window = browser.DXFStructureBrowser(
            args.file,
            line=args.line,
            handle=args.handle,
            resource_path=resources_path(),
        )
        main_window.show()
        sys.exit(app.exec())


@register
class BrowseAcisData(Command):
    """Launcher sub-command: browse-acis"""

    NAME = "browse-acis"

    @staticmethod
    def add_parser(subparsers):
        parser = subparsers.add_parser(
            BrowseAcisData.NAME, help="browse ACIS structures in DXF files"
        )
        parser.add_argument(
            "file",
            metavar="FILE",
            nargs="?",
            help="DXF file to browse",
        )
        parser.add_argument(
            "-g",
            "--handle",
            required=False,
            help="go to entity by HANDLE, HANDLE has to be a hex value without "
            "any prefix like 'fefe'",
        )

    @staticmethod
    def run(args):
        try:
            from ezdxf.addons.xqt import QtWidgets
        except ImportError as e:
            print(str(e))
            sys.exit(1)
        from ezdxf.addons.acisbrowser.browser import AcisStructureBrowser

        signal.signal(signal.SIGINT, signal.SIG_DFL)  # handle Ctrl+C properly
        app = QtWidgets.QApplication(sys.argv)
        app.setStyle("Fusion")
        set_app_icon(app)
        main_window = AcisStructureBrowser(
            args.file,
            handle=args.handle,
        )
        main_window.show()
        sys.exit(app.exec())


@register
class Strip(Command):
    """Launcher sub-command: strip"""

    NAME = "strip"

    @staticmethod
    def add_parser(subparsers):
        parser = subparsers.add_parser(Strip.NAME, help="strip comments from DXF files")
        parser.add_argument(
            "file",
            metavar="FILE",
            nargs="+",
            help='DXF file to process, wildcards "*" and "?" are supported',
        )
        parser.add_argument(
            "-b",
            "--backup",
            action="store_true",
            required=False,
            help='make a backup copy with extension ".bak" from the '
            "DXF file, overwrites existing backup files",
        )
        parser.add_argument(
            "-t",
            "--thumbnail",
            action="store_true",
            required=False,
            help="strip THUMBNAILIMAGE section",
        )
        parser.add_argument(
            "--handles",
            action="store_true",
            required=False,
            help="remove handles from DXF R12 or older files",
        )
        parser.add_argument(
            "-v",
            "--verbose",
            action="store_true",
            required=False,
            help="give more output",
        )

    @staticmethod
    def run(args):
        from ezdxf.tools.strip import strip

        for pattern in args.file:
            for filename in glob.glob(pattern):
                codes = [999]
                if args.handles:
                    if is_dxf_r12_file(filename):
                        codes.extend([5, 105])
                    else:
                        print(
                            f"Cannot remove handles from DXF R13 or later: {filename}"
                        )
                strip(
                    filename,
                    backup=args.backup,
                    thumbnail=args.thumbnail,
                    verbose=args.verbose,
                    codes=codes,
                )


@register
class Config(Command):
    """Launcher sub-command: config"""

    NAME = "config"

    @staticmethod
    def add_parser(subparsers):
        parser = subparsers.add_parser(Config.NAME, help="manage config files")
        parser.add_argument(
            "-p",
            "--print",
            action="store_true",
            help="print configuration",
        )
        parser.add_argument(
            "-w",
            "--write",
            metavar="FILE",
            help="write configuration",
        )
        parser.add_argument(
            "--home",
            action="store_true",
            help="create config file 'ezdxf.ini' in the user home directory "
            "'~/.config/ezdxf', $XDG_CONFIG_HOME is supported if set",
        )
        parser.add_argument(
            "--reset",
            action="store_true",
            help="factory reset, delete default config files 'ezdxf.ini'",
        )

    @staticmethod
    def run(args):
        from ezdxf import options

        action = False
        if args.reset:
            options.reset()
            options.delete_default_config_files()
            action = True
        if args.home:
            options.write_home_config()
            action = True
        if args.write:
            action = True
            filepath = Path(args.write).expanduser()
            try:
                options.write_file(str(filepath))
                print(f"configuration written to: {filepath}")
            except IOError as e:
                print(str(e))
        if args.print or action is False:
            options.print()


def load_every_document(filename: str):
    def io_error() -> str:
        msg = f'Not a DXF file or a generic I/O error: "{filename}"'
        print(msg, file=sys.stderr)
        return msg

    def structure_error() -> str:
        msg = f'Invalid or corrupted DXF file: "{filename}"'
        print(msg, file=sys.stderr)
        return msg

    binary_fmt = False
    if is_binary_dxf_file(filename):
        try:
            doc = ezdxf.readfile(filename)
        except IOError:
            raise const.DXFLoadError(io_error())
        except const.DXFStructureError:
            raise const.DXFLoadError(structure_error())
        auditor = doc.audit()
        binary_fmt = True
    else:
        try:
            doc, auditor = recover.readfile(filename)
        except IOError:
            raise const.DXFLoadError(io_error())
        except const.DXFStructureError:
            dwginfo = dwg_file_info(filename)
            if dwginfo.version != "invalid":
                print(
                    f"This is a DWG file!!!\n"
                    f'Filename: "{filename}"\n'
                    f"Format: DWG\n"
                    f"Release: {dwginfo.release}\n"
                    f"DWG Version: {dwginfo.version}\n"
                )
                raise const.DXFLoadError()
            raise const.DXFLoadError(structure_error())
    return doc, auditor, binary_fmt


@register
class Info(Command):
    """Launcher sub-command: info"""

    NAME = "info"

    @staticmethod
    def add_parser(subparsers):
        parser = subparsers.add_parser(
            Info.NAME,
            help="show information and optional stats of DXF files as "
            "loaded by ezdxf, this may not represent the original "
            "content of the file, use the browse command to "
            "see the original content",
        )
        parser.add_argument(
            "file",
            metavar="FILE",
            nargs="+",
            help='DXF file to process, wildcards "*" and "?" are supported',
        )
        parser.add_argument(
            "-v",
            "--verbose",
            action="store_true",
            required=False,
            help="give more output",
        )
        parser.add_argument(
            "-s",
            "--stats",
            action="store_true",
            required=False,
            help="show content stats",
        )

    @staticmethod
    def run(args):
        from ezdxf.document import info

        def process(fn: str):
            try:
                doc, auditor, binary_fmt = load_every_document(fn)
            except const.DXFLoadError:
                pass
            else:
                fmt = "Binary" if binary_fmt else "ASCII"
                print(
                    "\n".join(
                        info(
                            doc,
                            verbose=args.verbose,
                            content=args.stats,
                            fmt=fmt,
                        )
                    )
                )
                if auditor.has_fixes:
                    print(f"Audit process fixed {len(auditor.fixes)} error(s).")
                if auditor.has_errors:
                    print(
                        f"Audit process found {len(auditor.errors)} unrecoverable error(s)."
                    )
                print()

        for pattern in args.file:
            file_count = 0
            for filename in glob.glob(pattern):
                if os.path.isdir(filename):
                    dir_pattern = os.path.join(filename, "*.dxf")
                    for filename2 in glob.glob(dir_pattern):
                        process(filename2)
                        file_count += 1
                else:
                    process(filename)
                    file_count += 1

            if file_count == 0:
                sys.stderr.write(f'No matching files for pattern: "{pattern}"\n')


@register
class HPGL(Command):
    """Launcher sub-command: hpgl"""

    NAME = "hpgl"

    @staticmethod
    def add_parser(subparsers):
        parser = subparsers.add_parser(
            HPGL.NAME, help=f"view and/or convert HPGL/2 plot files to various formats"
        )
        parser.add_argument(
            "file",
            metavar="FILE",
            nargs="?",
            default="",
            help=f"view and/or convert HPGL/2 plot files, wildcards (*, ?) supported in command line mode",
        )
        parser.add_argument(
            "-e",
            "--export",
            metavar="FORMAT",
            required=False,
            help=f"convert HPGL/2 plot file to SVG, PDF or DXF from the command line (no gui)",
        )
        parser.add_argument(
            "-r",
            "--rotate",
            type=int,
            choices=(0, 90, 180, 270),
            default=0,
            required=False,
            help="rotate page about 90, 180 or 270 degrees (no gui)",
        )
        parser.add_argument(
            "-x",
            "--mirror_x",
            action="store_true",
            required=False,
            help="mirror page in x-axis direction, (no gui)",
        )
        parser.add_argument(
            "-y",
            "--mirror_y",
            action="store_true",
            required=False,
            help="mirror page in y-axis direction, (no gui)",
        )
        parser.add_argument(
            "-m",
            "--merge_control",
            type=int,
            required=False,
            default=2,
            choices=(0, 1, 2),
            help="provides control over the order of filled polygons, 0=off (print order), "
            "1=luminance (order by luminance), 2=auto (default)",
        )
        parser.add_argument(
            "-f",
            "--force",
            action="store_true",
            required=False,
            help="inserts the mandatory 'enter HPGL/2 mode' escape sequence into the data "
            "stream; use this flag when no HPGL/2 data was found and you are sure the "
            "file is a HPGL/2 plot file",
        )
        parser.add_argument(
            "--aci",
            action="store_true",
            required=False,
            help="use pen numbers as ACI colors and assign colors by layer (DXF only)",
        )
        parser.epilog = (
            "Note that plot files are intended to be plotted on white paper."
        )
        parser.add_argument(
            "--dpi",
            type=int,
            required=False,
            default=96,
            help="pixel density in dots per inch (PNG only)",
        )
        parser.epilog = (
            "Note that plot files are intended to be plotted on white paper."
        )

    @staticmethod
    def run(args):
        if args.export:
            if os.path.exists(args.file):
                filenames = [args.file]
            else:
                filenames = glob.glob(args.file)
            for filename in filenames:
                export_hpgl2(Path(filename), args)
        else:
            launch_hpgl2_viewer(args.file, args.force)


def export_hpgl2(filepath: Path, args) -> None:
    from ezdxf.addons.hpgl2 import api as hpgl2
    from ezdxf.addons.drawing.dxf import ColorMode

    fmt = args.export.upper()
    start_msg = f"converting HPGL/2 plot file '{filepath.name}' to {fmt}"
    try:
        data = filepath.read_bytes()
    except IOError as e:
        print(str(e), file=sys.stderr)
        return
    if args.force:
        data = b"%1B" + data
    export_path = filepath.with_suffix(f".{fmt.lower()}")
    if fmt == "DXF":
        print(start_msg)
        color_mode = ColorMode.ACI if args.aci else ColorMode.RGB
        doc = hpgl2.to_dxf(
            data,
            rotation=args.rotate,
            mirror_x=args.mirror_x,
            mirror_y=args.mirror_y,
            color_mode=color_mode,
            merge_control=args.merge_control,
        )
        try:
            doc.saveas(export_path)
        except IOError as e:
            print(str(e), file=sys.stderr)

    elif fmt == "SVG":
        print(start_msg)
        svg_string = hpgl2.to_svg(
            data,
            rotation=args.rotate,
            mirror_x=args.mirror_x,
            mirror_y=args.mirror_y,
            merge_control=args.merge_control,
        )
        try:
            export_path.write_text(svg_string)
        except IOError as e:
            print(str(e), file=sys.stderr)
    elif fmt == "PDF":
        print(start_msg)
        pdf_bytes = hpgl2.to_pdf(
            data,
            rotation=args.rotate,
            mirror_x=args.mirror_x,
            mirror_y=args.mirror_y,
            merge_control=args.merge_control,
        )
        try:
            export_path.write_bytes(pdf_bytes)
        except IOError as e:
            print(str(e), file=sys.stderr)
    elif fmt == "PNG":
        print(start_msg)
        png_bytes = hpgl2.to_pixmap(
            data,
            rotation=args.rotate,
            mirror_x=args.mirror_x,
            mirror_y=args.mirror_y,
            merge_control=args.merge_control,
            fmt="png",
            dpi=args.dpi,
        )
        try:
            export_path.write_bytes(png_bytes)
        except IOError as e:
            print(str(e), file=sys.stderr)
    else:
        print(f"invalid export format: {fmt}")
        exit(1)
    print(f"file '{export_path.name}' successfully written")


def launch_hpgl2_viewer(filename: str, force: bool) -> None:
    try:
        from ezdxf.addons.xqt import QtWidgets
    except ImportError as e:
        print(str(e))
        exit(1)
    from ezdxf.addons.hpgl2.viewer import HPGL2Viewer

    signal.signal(signal.SIGINT, signal.SIG_DFL)  # handle Ctrl+C properly
    app = QtWidgets.QApplication(sys.argv)
    app.setStyle("Fusion")
    set_app_icon(app)
    viewer = HPGL2Viewer()
    viewer.show()
    if filename and os.path.exists(filename):
        viewer.load_plot_file(filename, force=force)
    sys.exit(app.exec())


def set_app_icon(app):
    from ezdxf.addons.xqt import QtGui, QtCore

    app_icon = QtGui.QIcon()
    p = resources_path()
    app_icon.addFile(str(p / "16x16.png"), QtCore.QSize(16, 16))
    app_icon.addFile(str(p / "24x24.png"), QtCore.QSize(24, 24))
    app_icon.addFile(str(p / "32x32.png"), QtCore.QSize(32, 32))
    app_icon.addFile(str(p / "48x48.png"), QtCore.QSize(48, 48))
    app_icon.addFile(str(p / "64x64.png"), QtCore.QSize(64, 64))
    app_icon.addFile(str(p / "256x256.png"), QtCore.QSize(256, 256))
    app.setWindowIcon(app_icon)


def resources_path():
    from pathlib import Path

    return Path(__file__).parent / "resources"
