# *****************************************************************************
# Nobody expects the Spanish Inquisition!
# Copyright (c) 2024- by the authors, see LICENSE
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Module authors:
#   Georg Brandl <g.brandl@fz-juelich.de>
#   Claude Sonnet <sonnet@claude.ai>
#
# *****************************************************************************

"""Backend for simulating devices."""

import asyncio
import math
import random
import time

from .backend import State
from .device import PeriodicPollDevice


class Device(PeriodicPollDevice):
    """Device that simulates values for demo and testing purposes.

    Device URL should look like: 'sim://device_name'

    Configuration options:

    - read_only: make device read-only (default: False)
    - value_type: 'float', 'int', 'string', 'choice' (default: 'float')
    - initial_value: initial value (default depends on type)
    - min_value: minimum value for numeric types (default: 0.0)
    - max_value: maximum value for numeric types (default: 100.0)
    - jitter: amount of random variation (default: 0.0 = stable)
    - jitter_type: 'random', 'sine', 'sawtooth', 'square' (default: 'random')
    - choices: list of choices for 'choice' type (default: ['A', 'B', 'C'])
    - unit: unit string for the value (default: '')
    - error_chance: chance of error state per poll (default: 0.0)
    - response_delay: simulate slow response in seconds (default: 0.0)
    """

    def __init__(self, name, devconf, uri, backend, log):
        super().__init__(name, devconf, uri, backend, log)

        # Parse device name from URI
        self._device_name = uri.path.lstrip('/')

        # Get configuration options
        opts = devconf.options
        self._read_only = bool(opts.get('read_only', False))
        self._value_type = opts.get('value_type', 'float')
        self._min_value = float(opts.get('min_value', 0.0))
        self._max_value = float(opts.get('max_value', 100.0))
        self._choices = opts.get('choices', ['A', 'B', 'C'])
        self._unit = opts.get('unit', '')
        self._jitter = float(opts.get('jitter', 0.0))
        self._jitter_type = opts.get('jitter_type', 'random')
        self._error_chance = float(opts.get('error_chance', 0.0))
        self._response_delay = float(opts.get('response_delay', 0.0))

        # Set initial value based on type
        initial_value = opts.get('initial_value')
        if initial_value is None:
            if self._value_type == 'float':
                initial_value = (self._min_value + self._max_value) / 2
            elif self._value_type == 'int':
                initial_value = int((self._min_value + self._max_value) / 2)
            elif self._value_type == 'string':
                initial_value = 'test_string'
            elif self._value_type == 'choice':
                initial_value = self._choices[0] if self._choices else 'A'
            else:
                initial_value = 0.0

        # Device state
        self._current_value = initial_value
        self._current_state = State.OK
        self._current_state_str = 'Simulated device ready'
        self._target_value = initial_value
        self._is_moving = False

        # Parameter values
        self._params = {}
        for i, param in enumerate(devconf.params):
            if param.name not in ('value', 'target'):
                every, stagger = (param.poll_every, i % param.poll_every) \
                    if param.poll_every else (1, 1)
                self._params[param.name] = \
                    [self._get_default_param_value(param.name), every, stagger]

        # Timing
        self._start_time = time.time()

        self.log.info('initialized test device %s with type %s',
                     name, self._value_type)

    def _get_default_param_value(self, param_name):
        """Get a default value for a parameter based on its name."""
        # Generic default based on value type
        if self._value_type == 'string':
            return f'{param_name}_value'
        if self._value_type == 'choice':
            return random.choice(self._choices)
        return random.uniform(self._min_value, self._max_value)

    async def init_poll(self):
        """Initialize polling - nothing special needed for test devices."""

    async def poll(self, n):
        """Poll the device and generate simulated values."""
        if self._response_delay > 0:
            await asyncio.sleep(self._response_delay)

        if random.random() < self._error_chance:
            if self._current_state != State.ERROR:
                self._current_state = State.ERROR
                self._current_state_str = 'Simulated error condition'
            else:
                self._current_state = State.OK
                self._current_state_str = 'Recovered from error'

        # Update current value based on movement
        if self._is_moving:
            # Simulate gradual movement towards target
            diff = self._target_value - self._current_value
            if abs(diff) < 0.01:
                self._current_value = self._target_value
                self._is_moving = False
                self._current_state = State.OK
                self._current_state_str = 'Target reached'
            elif self._value_type == 'int':
                # Move +/-1 closer each poll
                step = 1 if diff > 0 else -1
                self._current_value += step
                self._current_state = State.BUSY
                self._current_state_str = 'Moving to target'
            else:
                # Move closer each poll (or given by speed parameter)
                delta = min(abs(diff), self._params.get('speed', [0.1])[0])
                if diff < 0:
                    self._current_value -= delta
                else:
                    self._current_value += delta
                self._current_state = State.BUSY
                self._current_state_str = 'Moving to target'
            value = self._current_value

        # Apply jitter if configured
        else:
            value = self._apply_jitter(self._current_value)

        # Send main value update
        await self.send(self._current_state, self._current_state_str, value)

        # Update parameters
        for param_name, [base_value, every, stagger] in self._params.items():
            if n == 0 or n % every == stagger:
                param_value = self._apply_jitter(base_value, param_name)
                await self.send_param(param_name, param_value, '')

    def _apply_jitter(self, base_value, param_name='value'):
        """Apply jitter to a value based on configuration."""
        if self._jitter == 0.0:
            return base_value

        # Only apply jitter to numeric values
        if not isinstance(base_value, (int, float)):
            return base_value

        current_time = time.time() - self._start_time

        if self._jitter_type == 'random':
            jitter_amount = random.uniform(-self._jitter, self._jitter)
        elif self._jitter_type == 'sine':
            phase = (current_time / 10) * 2 * math.pi
            # Add some randomness to the phase for each parameter
            phase += hash(param_name) % 100 / 100.0 * 2 * math.pi
            jitter_amount = self._jitter * math.sin(phase)
        elif self._jitter_type == 'sawtooth':
            phase = (current_time / 10) % 1.0
            phase += (hash(param_name) % 100) / 100.0
            phase = phase % 1.0
            jitter_amount = self._jitter * (2 * phase - 1)
        elif self._jitter_type == 'square':
            phase = (current_time / 10) % 1.0
            phase += (hash(param_name) % 100) / 100.0
            phase = phase % 1.0
            jitter_amount = self._jitter * (1 if phase < 0.5 else -1)
        else:
            jitter_amount = 0.0

        result = base_value + jitter_amount

        # Clamp to bounds for numeric types
        if self._value_type in ['float', 'int']:
            result = max(self._min_value, min(self._max_value, result))

        # Convert to correct type
        if self._value_type == 'int':
            result = round(result)

        return result

    async def action_get(self, param):
        """Get the current value of a parameter."""
        if param == 'value':
            return self._current_value
        if param == 'target':
            return self._target_value
        if param in self._params:
            return self._params[param][0]
        raise ValueError(f'Unknown parameter: {param}')

    async def action_set(self, param, value):
        """Set the value of a parameter."""
        if self._read_only:
            raise PermissionError('Device is read-only')
        if param in ('value', 'target'):
            if self._value_type == 'float':
                self._target_value = float(value)
            elif self._value_type == 'int':
                self._target_value = int(value)
            elif self._value_type == 'choice':
                if value in self._choices:
                    self._target_value = value
                else:
                    msg = f'Invalid choice: {value}, must be one of {self._choices}'
                    raise ValueError(msg)
            elif self._value_type == 'string':
                self._target_value = str(value)
            else:
                self._target_value = value

            # Start movement simulation if values differ
            if self._target_value != self._current_value:
                if self._value_type in ['float', 'int']:
                    self._is_moving = True
                    self._current_state = State.BUSY
                    self._current_state_str = f'Moving to {self._target_value}'
                else:
                    self._current_value = self._target_value
                    self._current_state = State.OK
                    self._current_state_str = 'Value set'
        elif param in self._params:
            self._params[param][0] = value
        else:
            raise ValueError(f'Unknown parameter: {param}')

    async def action_stop(self):
        """Stop the device movement."""
        if self._read_only:
            raise PermissionError('Device is read-only')
        self._is_moving = False
        self._target_value = self._current_value
        self._current_state = State.OK
        self._current_state_str = 'Stopped'

    async def action_reset(self):
        """Reset the device to initial state."""
        # Reset to initial values
        opts = self._devconf.options
        initial_value = opts.get('initial_value')
        if initial_value is None:
            if self._value_type == 'float':
                initial_value = (self._min_value + self._max_value) / 2
            elif self._value_type == 'int':
                initial_value = int((self._min_value + self._max_value) / 2)
            elif self._value_type == 'string':
                initial_value = 'test_string'
            elif self._value_type == 'choice':
                initial_value = self._choices[0] if self._choices else 'A'
            else:
                initial_value = 0.0

        self._current_value = initial_value
        self._target_value = initial_value
        self._is_moving = False
        self._current_state = State.OK
        self._current_state_str = 'Reset to initial state'

    async def action_state(self, _param):
        """Get the current state of the device or parameter."""
        return self._current_state

    async def input_value_type(self, param):
        """Return the input value type for a parameter."""
        if param == 'value':
            if self._value_type == 'int':
                return ['int', int(self._min_value), int(self._max_value)]
            if self._value_type == 'choice':
                return ['choice', self._choices]
            if self._value_type == 'string':
                return ['str']
            return ['float', self._min_value, self._max_value]
        # For parameters, assume float by default
        return ['float', -1e308, 1e308]

    def needed_assets(self):
        """Return list of assets needed by this device."""
        return []
