# Copyright 2026 Espressif Systems (Shanghai) Co., Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import json
import math
import random
import threading
import time


class MockDevice:
    """
    Generic Mock Device for ESP Viewer.
    Generates data streams based on the provided profile.
    """

    def __init__(self, port="MOCK", profile=None):
        self.port = port
        self.profile = profile  # We need this to know what to generate!
        self.running = False
        self.connected = False
        self.rx_buffer = ""
        self.streaming = False
        self.t = 0.0
        self.sample_rate = 50  # Hz
        self.thread = None
        self.params = {}  # Track device state
        self._init_params()

        # Local state for physics filtering
        self.accel_filtered = [0.0, 0.0, 9.8]  # Start at gravity
        self.gyro_filtered = [0.0, 0.0, 0.0]
        self.last_shake_t = -10.0

    def _init_params(self):
        if self.profile:
            for p in self.profile.get("parameters", []):
                val = p.get("default")
                if val is not None:
                    # Convert based on type
                    ptype = p.get("type", "int")
                    try:
                        if ptype == "int":
                            self.params[p["id"]] = int(val)
                        elif ptype == "float":
                            self.params[p["id"]] = float(val)
                        else:
                            self.params[p["id"]] = val
                    except:
                        self.params[p["id"]] = val

    def set_profile(self, profile):
        """Allow injection of profile if not provided at init"""
        self.profile = profile
        self._init_params()

    def open(self):
        self.connected = True
        self.running = True
        self.thread = threading.Thread(target=self._run_simulation, daemon=True)
        self.thread.start()

    def close(self):
        self.running = False
        self.connected = False

    def write(self, data):
        data = data.strip()
        if data.startswith("{"):
            try:
                req = json.loads(data)
                method = req.get("method")
                if method == "start_stream":
                    self.streaming = True
                elif method == "stop_stream":
                    self.streaming = False
                elif method == "set_config":
                    params = req.get("params", {})
                    for k, v in params.items():
                        self.params[k] = v

                res = {"jsonrpc": "2.0", "id": req.get("id"), "result": "ok"}
                self._emit(json.dumps(res))
            except:
                pass

    def write_raw(self, data):
        pass

    def readline(self):
        if not self.rx_buffer:
            return None
        if "\n" in self.rx_buffer:
            line, self.rx_buffer = self.rx_buffer.split("\n", 1)
            return line + "\n"
        return None

    def _emit(self, line):
        self.rx_buffer += line + "\n"

    def _run_simulation(self):
        while self.running:
            # Dynamic sampling rate
            try:
                target_fps = float(self.params.get("sampling_rate", 50))
            except (ValueError, TypeError):
                target_fps = 50.0
            if target_fps < 1:
                target_fps = 1
            time.sleep(1.0 / target_fps)

            if not self.streaming or not self.profile:
                continue

            self.t += 1.0 / target_fps

            # Use mock_type to decide which generator to use
            mock_type = self.profile.get("meta", {}).get("mock_type", "data")

            if mock_type == "waveform":
                self._gen_waveform()
            elif mock_type == "imu":
                self._gen_imu()
            elif mock_type == "multi_plot":
                self._gen_multi_plot()
            else:
                self._gen_generic_data()

            # Handle log streams (always active if defined)
            for stream in self.profile.get("streams", []):
                if stream.get("type") == "log":
                    # Randomly emit logs
                    if random.random() < 0.05:
                        self._emit(
                            f"LOG:Simulation tick {int(self.t)} Gain:{self.params.get('gain')} Thr:{self.params.get('threshold_v')}"
                        )

    def _gen_waveform(self):
        """Generates a high-speed modulated sine wave with physical offsets."""
        for stream in self.profile.get("streams", []):
            if stream.get("type", "data") != "data":
                continue

            fields = stream.get("fields", [])
            vals = []
            for f in fields:
                if f in ("ts", "time"):
                    vals.append(str(int(self.t * 1000)))
                else:
                    f_c = self.params.get("carrier_freq", 5.0)
                    amp = self.params.get("carrier_amp", 50.0)
                    offset = self.params.get("offset_v", 0.0)
                    noise_rms = self.params.get("noise_rms", 0.0)

                    mod_type = str(self.params.get("mod_type", "0"))
                    f_m = self.params.get("mod_freq", 1.0)
                    depth = (
                        self.params.get("mod_depth", 50.0) / 100.0
                    )  # Percentage to multiplier

                    phase = 2 * math.pi * f_c * self.t
                    if mod_type == "2":  # FM
                        phase += depth * 5.0 * math.sin(2 * math.pi * f_m * self.t)
                        val = amp * math.sin(phase)
                    elif mod_type == "1":  # AM
                        mod = 1.0 + depth * math.sin(2 * math.pi * f_m * self.t)
                        val = amp * mod * math.sin(phase)
                    else:
                        val = amp * math.sin(phase)

                    val += (
                        offset + random.normalvariate(0, noise_rms)
                        if noise_rms > 0
                        else offset
                    )
                    vals.append(f"{val:.2f}")

            self._emit_csv(stream, vals)

    def _gen_imu(self):
        """Generates realistic 6-axis IMU data with physics and triggers."""
        # Parameters
        g = self.params.get("gravity_g", 9.8)
        noise = self.params.get("noise_level", 0.05)
        lpf_cutoff = self.params.get("lpf_cutoff", 10.0)  # Hz
        target_fps = self.params.get("sampling_rate", 50)
        dt = 1.0 / target_fps if target_fps > 0 else 0.02

        # Calculate EMA alpha
        alpha = min(1.0, 2 * 3.14159 * lpf_cutoff * dt)

        # 1. Physics Model: Stationary Gravity + Random Impulse (Jerks)
        jerk = [0.0, 0.0, 0.0]
        if random.random() < 0.02:  # Occasional jerky motion
            jerk = [random.uniform(-5, 5) for _ in range(3)]

        target_accel = [0.0 + jerk[0], 0.0 + jerk[1], g + jerk[2]]
        target_gyro = [random.uniform(-2, 2) for _ in range(3)]
        if any(abs(j) > 0.1 for j in jerk):
            target_gyro = [j * 20 for j in jerk]

        # 2. Apply LPF (EMA)
        for i in range(3):
            self.accel_filtered[i] = (
                self.accel_filtered[i] * (1 - alpha) + target_accel[i] * alpha
            )
            self.gyro_filtered[i] = (
                self.gyro_filtered[i] * (1 - alpha) + target_gyro[i] * alpha
            )

        # 3. Add Noise and Emit
        for stream in self.profile.get("streams", []):
            if stream.get("type") != "data":
                continue

            fields = stream.get("fields", [])
            vals = []
            accel_raw = []
            for f in fields:
                if f in ("ts", "time"):
                    vals.append(str(int(self.t * 1000)))
                elif f in ("ax", "ay", "az"):
                    idx = {"ax": 0, "ay": 1, "az": 2}[f]
                    v = self.accel_filtered[idx] + random.normalvariate(0, noise)
                    vals.append(f"{v:.3f}")
                    accel_raw.append(v / g)
                elif f in ("gx", "gy", "gz"):
                    idx = {"gx": 0, "gy": 1, "gz": 2}[f]
                    v = self.gyro_filtered[idx] + random.normalvariate(0, noise * 10)
                    vals.append(f"{v:.2f}")

            self._emit_csv(stream, vals)

            # 4. Trigger Logic (Shake) - De-bounced
            if len(accel_raw) == 3:
                mag = math.sqrt(sum(a * a for a in accel_raw))
                threshold = self.params.get("shake_threshold", 2.5)
                if mag > threshold and (self.t - self.last_shake_t) > 1.0:
                    self.last_shake_t = self.t
                    self._emit(f"EVENT,{int(self.t * 1000)},SHAKE")

    def _gen_multi_plot(self):
        """Generates 9 channels of diverse waveforms for the multi_plot demo."""
        rate = self.params.get("sampling_rate", 20)
        dt = 1.0 / rate
        time.sleep(dt)
        self.t += dt

        # ts, a1, a2, a3, b1, b2, b3, c1, c2, c3
        ts = int(self.t * 1000)

        # Stream A (Header P1): Sines/Squares
        a1 = math.sin(self.t * 2 * math.pi * 0.5)
        a2 = math.cos(self.t * 2 * math.pi * 0.5)
        a3 = 1.0 if (int(self.t * 2) % 2 == 0) else -1.0
        self._emit(f"P1,{ts},{a1:.3f},{a2:.3f},{a3:.3f}")

        # Stream B (Header P2): Triangle/Saw/Noise
        b1 = abs((self.t % 2.0) - 1.0) * 2 - 1.0
        b2 = (self.t % 1.0) * 2 - 1.0
        b3 = (random.random() * 2 - 1.0) * 0.5
        self._emit(f"P2,{ts},{b1:.3f},{b2:.3f},{b3:.3f}")

        # Stream C (Header P3): Phase shifted sines
        c1 = math.sin(self.t * 2 * math.pi * 0.2 + 0)
        c2 = math.sin(self.t * 2 * math.pi * 0.2 + 2 * math.pi / 3)
        c3 = math.sin(self.t * 2 * math.pi * 0.2 + 4 * math.pi / 3)
        self._emit(f"P3,{ts},{c1:.3f},{c2:.3f},{c3:.3f}")

    def _gen_generic_data(self):
        """Old generic generator for compatibility."""
        for stream in self.profile.get("streams", []):
            if stream.get("type", "data") == "data":
                fields = stream.get("fields", [])
                vals = [str(random.randint(0, 100)) for _ in fields]
                self._emit_csv(stream, vals)

    def _emit_csv(self, stream, vals):
        header = stream.get("header", "")
        delim = stream.get("delimiter", ",")
        line = (header + delim if header else "") + delim.join(vals)
        self._emit(line)
