import importlib
import sys
import time
import threading
from typing import Optional

from flask import Blueprint, Flask, jsonify, render_template, request


def _load_lgpio():
    try:
        return importlib.import_module("lgpio"), None
    except ImportError as exc:
        error = exc
        major, minor = sys.version_info[:2]
        candidates = {
            f"/usr/lib/python{major}.{minor}/dist-packages",
            f"/usr/local/lib/python{major}.{minor}/dist-packages",
            "/usr/lib/python3/dist-packages",
            "/usr/local/lib/python3/dist-packages",
        }
        for path in candidates:
            if path not in sys.path:
                sys.path.append(path)
                try:
                    return importlib.import_module("lgpio"), None
                except ImportError as exc2:
                    error = exc2
        return None, error


lgpio, LGPIO_IMPORT_ERROR = _load_lgpio()

# --- GPIO 定义 ---
EN, DIR, STEP = 17, 27, 23
PIN_LIST = [EN, DIR, STEP]

HIGH = 1
LOW = 0
DIR_POSITIVE = HIGH
DIR_NEGATIVE = LOW
ENABLE_ACTIVE = LOW
ENABLE_IDLE = HIGH

# --- 核心标定参数 ---
STEPS_PER_CM = 50
SLIDER_MAX_CM = 22.0
SLIDER_MAX_STEPS = int(STEPS_PER_CM * SLIDER_MAX_CM)
WORKING_RANGE_CM = 20.0
WORKING_ZERO_OFFSET_CM = 1.0

SPEEDS = {
    "slow": 0.002,
    "medium": 0.001,
    "fast": 0.0005,
}


class SliderControl:
    def __init__(self, chip: int = 0):
        if lgpio is None:
            hint = (
                "lgpio 导入失败: "
                f"{LGPIO_IMPORT_ERROR}. 请确认已安装 python3-lgpio 并允许虚拟环境访问系统库"
            )
            raise RuntimeError(hint)

        self._chip_index = chip
        try:
            self._chip_handle = lgpio.gpiochip_open(chip)
        except Exception as exc:
            raise RuntimeError(
                f"无法打开 gpiochip{chip}: {exc}. 请检查权限或使用 sudo 运行。"
            ) from exc

        self._claimed_pins = []
        self._shared_step_device = None
        for pin in PIN_LIST:
            initial_level = ENABLE_IDLE if pin == EN else LOW
            try:
                lgpio.gpio_claim_output(self._chip_handle, pin, initial_level)
                self._claimed_pins.append(pin)
            except Exception as exc:
                if pin == STEP and "busy" in str(exc).lower():
                    self._shared_step_device = self._acquire_shared_step_device()
                    if self._shared_step_device is None:
                        raise RuntimeError(
                            f"STEP 引脚 GPIO{STEP} 已被占用。"
                            " 请检查是否与 knock 模块的 GPIO 冲突，或调整滑轨引脚配置。"
                        ) from exc
                    print(f"检测到 STEP 引脚 GPIO{STEP} 正由其他模块占用，将复用现有通道。")
                    try:
                        self._shared_step_device.off()
                    except Exception:
                        pass
                else:
                    raise

        self.current_position_steps = 0
        self.current_speed_delay = SPEEDS["medium"]
        self.working_zero_steps = int(WORKING_ZERO_OFFSET_CM * STEPS_PER_CM)
        self.lock = threading.Lock()

    def _write(self, pin: int, level: int) -> None:
        if pin == STEP and self._shared_step_device is not None:
            if level == HIGH:
                self._shared_step_device.on()
            else:
                self._shared_step_device.off()
            return
        lgpio.gpio_write(self._chip_handle, pin, level)

    def _enable_motor(self) -> None:
        self._write(EN, ENABLE_ACTIVE)
        time.sleep(0.01)

    def _disable_motor(self) -> None:
        self._write(EN, ENABLE_IDLE)

    def _pulse(self) -> None:
        self._write(STEP, HIGH)
        time.sleep(self.current_speed_delay)
        self._write(STEP, LOW)
        time.sleep(self.current_speed_delay)

    def _move_steps(self, steps_to_move: int) -> None:
        with self.lock:
            self._enable_motor()
            target_steps = self.current_position_steps + steps_to_move
            if target_steps > SLIDER_MAX_STEPS:
                print(f"警告：超出最大行程 {SLIDER_MAX_CM}cm")
                target_steps = SLIDER_MAX_STEPS
            elif target_steps < 0:
                print("警告：超出0点物理极限")
                target_steps = 0

            actual_steps = target_steps - self.current_position_steps
            if actual_steps == 0:
                print("已在目标位置或边界。")
                self._disable_motor()
                return

            self._write(DIR, DIR_POSITIVE if actual_steps > 0 else DIR_NEGATIVE)
            for _ in range(abs(actual_steps)):
                self._pulse()

            self.current_position_steps = target_steps
            self._disable_motor()
            print(f"移动完成。当前绝对位置: {self.current_position_steps} 步")

    def set_speed(self, speed_val) -> None:
        with self.lock:
            if speed_val in SPEEDS:
                self.current_speed_delay = SPEEDS[speed_val]
                print(f"速度切换为: {speed_val}")
            else:
                try:
                    custom_delay = float(speed_val)
                    if 0.0001 < custom_delay < 0.01:
                        self.current_speed_delay = custom_delay
                        print(f"速度切换为: 自定义 {custom_delay}")
                    else:
                        print("自定义速度超出范围 (0.0001 - 0.01)")
                except (TypeError, ValueError):
                    print("无效的速度值")

    def calibrate_working_zero(self) -> None:
        with self.lock:
            self.current_position_steps = 0
            print("已重置绝对0点。")
        self._move_steps(self.working_zero_steps)
        print(f"已移动到工作0点 (绝对位置 {self.working_zero_steps} 步)")

    def go_to_working_zero(self) -> None:
        steps_to_go = self.working_zero_steps - self.current_position_steps
        if steps_to_go != 0:
            self._move_steps(steps_to_go)
        else:
            print("已在工作0点。")

    def move_relative(self, direction: str, cm: float) -> None:
        steps_to_go = int(cm * STEPS_PER_CM)
        if direction == 'left':
            self._move_steps(-steps_to_go)
        elif direction == 'right':
            self._move_steps(steps_to_go)

    def go_to_working_position_cm(self, target_work_cm: float) -> None:
        if not (0 <= target_work_cm <= WORKING_RANGE_CM):
            print(f"错误：目标位置必须在 0 - {WORKING_RANGE_CM}cm 工作区内")
            return
        target_absolute_steps = int(target_work_cm * STEPS_PER_CM) + self.working_zero_steps
        steps_to_go = target_absolute_steps - self.current_position_steps
        self._move_steps(steps_to_go)

    def get_status(self) -> dict:
        with self.lock:
            working_pos_steps = self.current_position_steps - self.working_zero_steps
            working_pos_cm = working_pos_steps / STEPS_PER_CM
            return {
                "working_position_cm": working_pos_cm,
                "absolute_position_steps": self.current_position_steps,
                "current_speed_delay": self.current_speed_delay,
                "backend": f"lgpio(chip={self._chip_index})",
            }

    def cleanup(self) -> None:
        if getattr(self, "_chip_handle", None) is None:
            return
        for pin in self._claimed_pins:
            try:
                lgpio.gpio_free(self._chip_handle, pin)
            except Exception:
                pass
        lgpio.gpiochip_close(self._chip_handle)
        self._chip_handle = None
        self._claimed_pins = []
        self._shared_step_device = None
        print("lgpio 资源已释放")

    def _acquire_shared_step_device(self):
        try:
            from knock.app import RELAY_PINS, channels
        except Exception:
            return None

        if STEP not in RELAY_PINS:
            return None

        idx = RELAY_PINS.index(STEP)
        try:
            device = channels[idx]
        except Exception:
            return None

        return device


slider_bp = Blueprint('slider', __name__, template_folder='templates')
_slider_instance: Optional[SliderControl] = None
_slider_error: Optional[str] = None


def _get_slider() -> SliderControl:
    global _slider_instance, _slider_error
    if _slider_instance is None and _slider_error is None:
        try:
            _slider_instance = SliderControl()
        except Exception as exc:
            _slider_error = str(exc)
            print(f"Slider init failed: {exc}")
    if _slider_instance is None:
        raise RuntimeError(_slider_error or "Slider unavailable")
    return _slider_instance


@slider_bp.route('/')
def index():
    return render_template('index.html')


@slider_bp.route('/status', methods=['GET'])
def status():
    try:
        slider = _get_slider()
    except RuntimeError as exc:
        return jsonify({"error": str(exc)}), 500
    return jsonify(slider.get_status())


@slider_bp.route('/calibrate', methods=['POST'])
def calibrate():
    try:
        slider = _get_slider()
    except RuntimeError as exc:
        return jsonify({"error": str(exc)}), 500
    slider.calibrate_working_zero()
    return jsonify(slider.get_status())


@slider_bp.route('/go_zero', methods=['POST'])
def go_zero():
    try:
        slider = _get_slider()
    except RuntimeError as exc:
        return jsonify({"error": str(exc)}), 500
    slider.go_to_working_zero()
    return jsonify(slider.get_status())


@slider_bp.route('/set_speed', methods=['POST'])
def set_speed():
    try:
        slider = _get_slider()
    except RuntimeError as exc:
        return jsonify({"error": str(exc)}), 500
    data = request.json or {}
    speed_val = data.get('speed', 'medium')
    slider.set_speed(speed_val)
    return jsonify(slider.get_status())


@slider_bp.route('/move_relative', methods=['POST'])
def move_relative():
    try:
        slider = _get_slider()
    except RuntimeError as exc:
        return jsonify({"error": str(exc)}), 500
    data = request.json or {}
    direction = data.get('direction')
    cm = data.get('cm', 0.5)
    slider.move_relative(direction, float(cm))
    return jsonify(slider.get_status())


@slider_bp.route('/move_absolute', methods=['POST'])
def move_absolute():
    try:
        slider = _get_slider()
    except RuntimeError as exc:
        return jsonify({"error": str(exc)}), 500
    data = request.json or {}
    try:
        target_cm = float(data.get('target_cm'))
    except (TypeError, ValueError):
        return jsonify({"error": "无效的目标位置"}), 400
    slider.go_to_working_position_cm(target_cm)
    return jsonify(slider.get_status())


def create_app():
    app = Flask(__name__)
    app.register_blueprint(slider_bp)
    return app


if __name__ == '__main__':
    try:
        create_app().run(host='0.0.0.0', port=5000)
    finally:
        if _slider_instance:
            _slider_instance.cleanup()
