import logging
import time
from pathlib import Path

from plotink import ebb_motion

from . import ut
from .axidraw.axidraw import AxiDraw

logger = logging.getLogger(__name__)

SCRIPT_PATH = Path(__file__).resolve()
repo_path = SCRIPT_PATH.parent.parent.parent


class AxiController(AxiDraw):
    def __init__(self):
        self._force_cli_api = False
        self._forced_total_mm = None
        super().__init__(user_message_fun=logger.info)

        setattr(self.options, 'units', 0)  # set the default right away

    def set_defaults(self):
        super().set_defaults()
        self.plot_status.progress.enable = False
        self.plot_status.progress.dry_run = False
        self.plot_status.progress.total = 0
        self.plot_status.progress.last = 0
        self.plot_status.progress.p_bar = None
        self.plot_status.progress.sub_bar = None
        if self._force_cli_api:
            self.plot_status.cli_api = True
            self.options.progress = True
            if self._forced_total_mm is not None:
                self.plot_status.progress.total = self._forced_total_mm

    def _estimate_total_mm(self, file) -> float | None:
        self.plot_setup(file)
        time.sleep(0.2)
        self.options.mode = 'plot'
        self.options.preview = True
        self.options.rendering = 0
        self.options.digest = 0
        self.options.progress = False
        self.options.progress_no_dry_run = True
        self._force_cli_api = False
        self.plot_status.cli_api = False
        self.apply_srv_options()
        try:
            self.plot_run()
        except Exception as exc:
            logger.warning('Estimate failed: %s', exc)
            return None
        total_inch = self.plot_status.stats.down_travel_inch + self.plot_status.stats.up_travel_inch
        return 25.4 * total_inch

    def plot_file(
        self,
        file,
        enable_progress: bool = False,
        allow_estimate: bool = True,
        return_output: bool = False,
    ):
        total_mm = None
        if enable_progress and allow_estimate:
            total_mm = self._estimate_total_mm(file)

        self.plot_setup(file)
        time.sleep(0.5)  # Wait half second
        self.options.mode = 'plot'
        self.options.preview = False
        self.options.digest = 0
        self.options.progress_no_dry_run = True
        self._force_cli_api = enable_progress
        self.plot_status.cli_api = enable_progress
        self.options.progress = enable_progress
        self._forced_total_mm = total_mm if total_mm is not None else None
        logger.info(
            'Plot start: progress=%s cli_api=%s preview=%s digest=%s mode=%s estimate=%s',
            self.options.progress,
            self.plot_status.cli_api,
            self.options.preview,
            self.options.digest,
            self.options.mode,
            allow_estimate,
        )
        self.apply_srv_options()
        output_svg = self.plot_run(output=return_output)
        if return_output:
            return output_svg, self.plot_status.stopped
        return None

    def resume_plot(self, svg_input, enable_progress: bool = False):
        self.plot_setup(svg_input)
        time.sleep(0.5)  # Wait half second
        self.options.mode = 'res_plot'
        self.options.preview = False
        self.options.digest = 0
        self.options.progress_no_dry_run = True
        self._force_cli_api = enable_progress
        self.plot_status.cli_api = enable_progress
        self.options.progress = enable_progress
        logger.info(
            'Resume start: progress=%s cli_api=%s preview=%s digest=%s mode=%s',
            self.options.progress,
            self.plot_status.cli_api,
            self.options.preview,
            self.options.digest,
            self.options.mode,
        )
        self.apply_srv_options()
        output_svg = self.plot_run(output=True)
        return output_svg, self.plot_status.stopped

    def apply_srv_options(self, srv_options: dict = None):
        if srv_options is None:
            file = repo_path / 'config' / 'srv_options.json'
            srv_options = ut.load_srv_options(file)

        for k, v in srv_options.items():
            if hasattr(self.options, k):
                if k == 'units':
                    unit_map = {'mm': 2, 'cm': 1, 'in': 0}
                    v = unit_map.get(v, 0)

                setattr(self.options, k, v)

    def motor_state(self):
        temp_connected = False
        try:
            if not self.connected or self.plot_status.port is None:
                self.serial_connect()
                temp_connected = True
            if self.plot_status.port is None:
                return None
            res = ebb_motion.query_enable_motors(self.plot_status.port, False)
            if not res or res[0] is None or res[1] is None:
                return None
            return res[0] > 0 and res[1] > 0
        except Exception:
            return None
        finally:
            if temp_connected:
                self.disconnect()

    def _util(self, func, **kwargs):
        self.plot_setup()
        self.options.mode = 'manual'
        self.options.manual_cmd = func

        for k, v in kwargs.items():
            if v is not None:
                if hasattr(self.options, k):
                    setattr(self.options, k, v)

        self.apply_srv_options()
        self.plot_run()

    def util_motors_on(self):
        self._util('enable_xy')

    def util_motors_off(self):
        self._util('disable_xy')

    def util_cycle(self):
        self.i_pendown()
        self.i_delay(500)
        self.i_penup()
        # self.disconnect()

    def util_toggle(self):
        self.i_penup() if not self.i_current_pen() else self.i_pendown()
        # self.disconnect()

    def util_align(self):
        self.plot_setup()
        self.options.mode = 'align'
        self.apply_srv_options()
        self.plot_run()

    def util_home(self):
        self._util('walk_home')

    def util_nudge_x(self, dist, unit='mm'):
        if unit == 'in':
            self._util('walk_x', dist=dist)
        elif unit == 'mm':
            self._util('walk_mmx', dist=dist)
        else:
            raise ValueError('Invalid unit for walk_x: use "mm" or "in"')

    def util_nudge_y(self, dist, unit='mm'):
        if unit == 'in':
            self._util('walk_y', dist=dist)
        elif unit == 'mm':
            self._util('walk_mmy', dist=dist)
        else:
            raise ValueError('Invalid unit for walk_y: use "mm" or "in"')

    ############ Interactive commands
    def _iactive(self, func, *args, **kwargs) -> None:
        if not self.options.mode == 'interactive':
            print('Switching to interactive mode...')
            self.interactive()

        self.apply_srv_options()

        if not self.connected:
            print('Connecting to AxiDraw device...')
            self.connect()
            if not self.connected:
                raise ConnectionError('Could not connect to AxiDraw device.')

        self.update()  # when options are changed after connect
        res = func(*args, **kwargs)
        return res

        # self.disconnect()

    def i_goto(self, x_target: float, y_target: float) -> None:
        return self._iactive(self.goto, x_target=x_target, y_target=y_target)

    def i_moveto(self, x_target: float, y_target: float) -> None:
        return self._iactive(self.moveto, x_target=x_target, y_target=y_target)

    def i_lineto(self, x_target: float, y_target: float) -> None:
        return self._iactive(self.lineto, x_target=x_target, y_target=y_target)

    def i_go(self, delta_x: float, delta_y: float) -> None:
        return self._iactive(self.go, delta_x, delta_y)

    def i_move(self, delta_x: float, delta_y: float) -> None:
        return self._iactive(self.move, delta_x, delta_y)

    def i_line(self, delta_x: float, delta_y: float) -> None:
        return self._iactive(self.line, delta_x=delta_x, delta_y=delta_y)

    def i_penup(self) -> None:
        return self._iactive(self.penup)

    def i_pendown(self) -> None:
        return self._iactive(self.pendown)

    def i_draw_path(self, vertex_list) -> None:
        return self._iactive(self.draw_path, vertex_list=vertex_list)

    def i_delay(self, time_ms) -> None:
        return self._iactive(self.delay, time_ms=time_ms)

    def i_block(self) -> None:
        return self._iactive(self.block)

    def i_current_pos(self) -> None:
        return self._iactive(self.current_pos)

    def i_turtle_pos(self) -> None:
        return self._iactive(self.turtle_pos)

    def i_current_pen(self) -> None:
        return self._iactive(self.current_pen)

    def i_turtle_pen(self) -> None:
        return self._iactive(self.turtle_pen)

    def i_usb_command(self) -> None:
        return self._iactive(self.usb_command)

    def i_usb_query(self) -> None:
        return self._iactive(self.usb_query)
