import errno
import json
import os
import runpy
import sys
import warnings
from pathlib import Path

OPTION_NAMES = [
    'mode',
    'speed_pendown',
    'speed_penup',
    'accel',
    'pen_pos_down',
    'pen_pos_up',
    'pen_rate_lower',
    'pen_rate_raise',
    'pen_delay_down',
    'pen_delay_up',
    'random_start',
    'hiding',
    'reordering',
    'no_rotate',
    'const_speed',
    'report_time',
    'manual_cmd',
    'dist',
    'layer',
    'copies',
    'page_delay',
    'preview',
    'rendering',
    'model',
    'penlift',
    'port',
    'port_config',
    'webhook',
    'webhook_url',
    'digest',
    'progress',
]


def default_srv_options() -> dict:
    return {
        'pen_pos_up': 65,
        'pen_pos_down': 35,
        'speed_penup': 75,
        'speed_pendown': 35,
        'accel': 60,
        'pen_rate_raise': 75,
        'pen_rate_lower': 50,
        'pen_delay_up': 0,
        'pen_delay_down': 0,
        'const_speed': False,
        'auto_rotate': False,
        'random_start': False,
        'reordering': 4,
        'model': 2,
        'units': 'mm',
    }


def load_srv_options(file):
    try:
        with open(file, 'r', encoding='utf-8') as f:
            return json.load(f)
    except FileNotFoundError:
        return default_srv_options()
    except json.JSONDecodeError:
        return default_srv_options()


def write_srv_options(srv_options, file):
    Path(file).parent.mkdir(parents=True, exist_ok=True)
    with open(file, 'w', encoding='utf-8') as f:
        json.dump(srv_options, f, indent=2)


def ensure_srv_options(file: Path) -> None:
    if Path(file).exists():
        return
    write_srv_options(default_srv_options(), file)


def clamp_int(value, min_value, max_value):
    return max(min_value, min(max_value, value))


def select_file_root(cli_value: str | None) -> Path:
    candidate = cli_value or os.environ.get('ARTWORK_ROOT')
    if not candidate:
        return (Path(os.getcwd()) / 'svg_input').resolve()
    resolved = Path(candidate).expanduser()
    if not resolved.is_absolute():
        resolved = Path(os.getcwd()) / resolved
    return resolved.resolve()


def resolve_dir_path(file_root: Path, rel_path: str | None) -> Path | None:
    if not rel_path:
        rel_path = '.'
    candidate = (file_root / rel_path).resolve()
    if candidate == file_root or file_root in candidate.parents:
        return candidate
    return None


def resolve_file_path(file_root: Path, rel_path: str | None) -> Path | None:
    if not rel_path:
        return None
    candidate = (file_root / rel_path).resolve()
    if candidate == file_root or file_root in candidate.parents:
        return candidate
    return None


def load_config(config):
    if config is None:
        return {}

    config_dict = None
    try:  # try assuming config is a filename
        config_dict = runpy.run_path(config)
    except SyntaxError as se:
        print('Config file {} contains a syntax error on line {}:'.format(se.filename, se.lineno))
        print('    {}'.format(se.text))
        print('The config file should be a python file (e.g., a file that ends in ".py").')
        sys.exit(1)
    except OSError as ose:
        if len(config) > 3 and config[-3:] == '.py' and ose.errno == errno.ENOENT:
            # if config is a filename ending in ".py" but it doesn't appear to exist
            print('Could not find any file named {}.'.format(config))
            print('Check the spelling and/or location.')
            sys.exit(1)
        else:
            # Either config is a config file that doesn't have a .py AND it doesn't exist
            # or config is a module
            with warnings.catch_warnings():
                warnings.simplefilter('ignore')
                # since technically this is importing "axidrawinternal.axidraw_conf" twice, it would generate a warning, but we can ignore it
                try:  # assume config is a module
                    config_dict = runpy.run_module(config)
                except ImportError as ie:  # oops, no module named that
                    # config may be a config file that doesn't have a .py AND doesn't exist
                    print('Could not find any file or module named {}.'.format(config))
                    sys.exit(1)

    return {key: value for key, value in config_dict.items() if key[0] != '_'}


def assign_option_values(options_obj, command_line, configs, option_names):
    """`configs` is a list of dicts containing values for the options, in order of priority.
    See get_configured_value.
    `command_line` is the return value of argparse.ArgumentParser.parse_args() or similar
    `options_obj` is the object that will be populated with the final option values.
    """

    for name in option_names:
        # argparse.ArgumentParser.parse_args
        # assigns None to any options that were
        # not defined by the user, so command line arguments are handled differently from
        # configured values (which might be correctly assigned None)
        command_line_value = getattr(command_line, name, None)
        if command_line_value is not None:
            setattr(options_obj, name, command_line_value)
        else:
            setattr(options_obj, name, get_configured_value(name, configs + [options_obj.__dict__]))


def get_configured_value(attr, configs):
    """configs is a list of configuration dicts, in order of priority.

    e.g. if configs is a list [user_config, other_config], then the default for
    "speed_pendown" will be user_config.speed_pendown if user_config.speed_pendown exists,
    and if not, the default will be other_config.speed_pendown.
    """
    for config in configs:
        if attr in config:
            return config[attr]
    raise ValueError('The given attr ({}) was not found in any of the configurations.'.format(attr))


class FakeConfigModule:
    """just turns a dict into an object
    so attributes can be set/retrieved object-style"""

    def __init__(self, a_dict):
        self.__dict__ = a_dict
