# This file is part of ts_scheduler
#
# Developed for the LSST Telescope and Site Systems.
# This product includes software developed by the LSST Project
# (https://www.lsst.org).
# See the COPYRIGHT file at the top-level directory of this distribution
# for details of code ownership.
#
# 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 3 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

import io
import logging
import os
import types
import typing
from dataclasses import dataclass

import pandas
from lsst.ts import observing

from .driver_target import DriverTarget
from .observation import Observation
from .survey_topology import SurveyTopology

__all__ = ["Driver", "DriverParameters"]

WORDY = logging.DEBUG - 5


@dataclass
class DriverParameters:
    """Actual global driver configuration parameters.

    This can be expanded for other scheduler drivers. For instance, if your
    scheduler uses a certain configuration file it is possible to subclass
    this and add the required parameters (e.g. file paths or else). Then,
    replace `self.params` on the Driver by the subclassed configuration.
    """

    night_boundary: float = -12.0
    new_moon_phase_threshold: float = 20.0

    def setDefaults(self) -> None:
        """Set defaults for the LSST Scheduler's Driver."""
        self.night_boundary = -12.0
        self.new_moon_phase_threshold = 20.0


class Driver:
    """The Scheduler Driver is the module that normalizes the interface between
    any scheduling algorithm to the LSST Scheduler CSC. The interface
    implements three main behaviours; configure an underlying algorithm,
    request targets and register successful observations.

    If the Scheduler algorithm requires a specific set of parameters the user
    must subclass `DriverParameters`, in the same module as the `Driver`, and
    add the appropriate parameters using the LSST pexConfig module.

    Access to the telemetry stream and models are also interfaced by `Driver`.
    The full list of default available telemetry data is shown in the
    scheduler_csc module. Nevertheless, the full list of telemetry may vary
    depending on the models used. The user has control over this while
    configuring the Scheduler CSC.

    Parameters
    ----------
    models: `dict`
        A dictionary with models available for the scheduling algorithm.
    raw_telemetry: `dict`
        A dictionary with available raw telemetry.
    observing_blocks : dict[str, observing.ObservingBlock]
        Observing blocks.
    parameters: `DriverParameters`, optional
        Input functional parameters for the scheduling algorithm.
    log: `Logger`
        Parent logger class.

    Attributes
    ----------
    log: `Logger`
        Logger class.
    parameters: `DriverParameters`
        Store functional driver parameters.
    models: `dict`
        Dictionary with models.
    raw_telemetry: `dict`
        Dictionary with raw telemetry data.
    target_id: `int`
        Counter of the number of targets generated by the scheduler since it
        was last instantiated.
    observing_blocks : dict[str, observing.ObservingBlock]
        Observing blocks.
    stop_tracking_script_name: `str`
        Name of a SAL Script that can be used to stop the observatory. This is,
        in general, queued by the Driver when it fails to select an
        observation.
    stop_tracking_script_is_standard: `bool`
        Specify if the stop tracking script is standard.
    is_night: `bool`
        Track if it is night time.
    night: `int`
        Count the number of nights from the start of the survey.
    current_sunset: `float`
        Timestamp for the current sunset.
    current_sunrise: `float`
        Timestamp for the current sunrise.
    """

    def __init__(
        self,
        models: dict[str, typing.Any],
        raw_telemetry: dict[str, typing.Any],
        observing_blocks: dict[str, observing.ObservingBlock],
        parameters: DriverParameters | None = None,
        log: logging.Logger | None = None,
    ) -> None:
        if log is None:
            self.log = logging.getLogger(type(self).__name__)
        else:
            self.log = log.getChild(type(self).__name__)

        if parameters is None:
            self.parameters = DriverParameters()
        else:
            self.parameters = parameters
        self.models = models
        self.raw_telemetry = raw_telemetry
        self.targetid = 0

        self.observing_blocks = observing_blocks

        self.stop_tracking_script_name = None
        self.stop_tracking_script_is_standard = None

        self.is_night = None
        self.night = 1
        self.current_sunset = None
        self.current_sunrise = None

    def configure_scheduler(self, config: types.SimpleNamespace) -> SurveyTopology:
        """This method is responsible for running the scheduler configuration
        and returning the survey topology, which specifies the number, name
        and type of projects running by the scheduler.

        By default it will just return a test survey topology.

        Parameters
        ----------
        config : `types.SimpleNamespace`
            Configuration, as described by ``schema/Scheduler.yaml``

        Returns
        -------
        survey_topology : ``SurveyTopology``
            Survey topology.
        """

        if "parameters" in config.driver_configuration:
            for parameter in config.driver_configuration["parameters"]:
                value = config.driver_configuration["parameters"][parameter]
                self.log.debug(f"Setting driver parameter: {parameter} = {value}")
                setattr(
                    self.parameters,
                    parameter,
                    value,
                )

        if not hasattr(config, "driver_configuration"):
            raise RuntimeError(
                "No driver_configuration section defined in configuration."
            )

        return self.get_survey_topology(config)

    def get_survey_topology(self, config: types.SimpleNamespace) -> SurveyTopology:
        """Get the survey topology.

        Parameters
        ----------
        config : `types.SimpleNamespace`
            Configuration, as described by ``schema/Scheduler.yaml``

        Returns
        -------
        survey_topology : ``SurveyTopology``
            Survey topology.
        """
        survey_topology = SurveyTopology()

        survey_topology.general_propos = config.driver_configuration.get(
            "general_propos", []
        )
        survey_topology.sequence_propos = config.driver_configuration.get(
            "sequence_propos", []
        )
        survey_topology.num_general_props = len(survey_topology.general_propos)
        survey_topology.num_seq_props = len(survey_topology.sequence_propos)

        self.stop_tracking_script_name = config.driver_configuration[
            "stop_tracking_observing_script_name"
        ]
        self.stop_tracking_script_is_standard = config.driver_configuration[
            "stop_tracking_observing_script_is_standard"
        ]

        return survey_topology

    def cold_start(self, observations: list[Observation]) -> None:
        """Rebuilds the internal state of the scheduler from a list of
        observations.

        Parameters
        ----------
        observations : `list`[`Observation`]
            List of observations.
        """
        raise NotImplementedError("Cold start is not implemented.")

    def update_conditions(self) -> None:
        """Update driver internal conditions.

        When subclassing this method, make sure to call it at the start of the
        method, as it performs operations like running the observatory through
        the current targets on the queue.
        """
        self.log.debug("Updating conditions.")

        self.models["sky"].update(self.models["observatory_state"].time)

        if self.is_night is None:
            self.log.debug("Driver not initialized yet. Computing night parameters.")
            # Driver was not initialized yet. Need to compute night
            # boundaries

            (self.current_sunset, self.current_sunrise) = self.models[
                "sky"
            ].get_night_boundaries(self.parameters.night_boundary)

            self.is_night = (
                self.current_sunset
                <= self.models["observatory_state"].time
                < self.current_sunrise
            )

            self.log.debug(
                f"Sunset/Sunrise: {self.current_sunset}/{self.current_sunrise}, "
                f"sun @ {self.parameters.night_boundary} degrees."
            )

        is_night = self.is_night

        self.is_night = (
            self.current_sunset
            <= self.models["observatory_state"].time
            < self.current_sunrise
        )

        # Only compute night boundaries when we transition from nighttime to
        # daytime. Possibilities are:
        # 1 - self.is_night=True and is_night = True: During the night (no need
        #     to compute anything).
        # 2 - self.is_night=False and is_night = True: Transitioned from
        #     night/day (need to recompute night boundaries).
        # 3 - self.is_night=True and is_night = False: Transitioned from
        #     day/night (no need to compute anything).
        # 4 - self.is_night=False and is_night = False: During the day, no need
        #     to compute anything.
        if not self.is_night and is_night:
            self.log.debug(
                "Night over. Computing next night boundaries. "
                f"Assuming sun elevation of {self.parameters.night_boundary}."
            )
            self.night += 1
            (self.current_sunset, self.current_sunrise) = self.models[
                "sky"
            ].get_night_boundaries(self.parameters.night_boundary)

            self.log.debug(
                f"[{self.night}]: Sunset/Sunrise: {self.current_sunset}/{self.current_sunrise} "
            )

    def select_next_target(self) -> DriverTarget:
        """Picks a target and returns it as a target object.

        By default it will just return a dummy test target.

        Returns
        -------
        `DriverTarget`
        """
        self.log.log(WORDY, "Selecting next target.")

        self.targetid += 1

        target = DriverTarget(
            observing_block=next(iter(self.observing_blocks.values())),
            targetid=self.targetid,
        )

        target.num_exp = 2
        target.exp_times = [15.0, 15.0]
        target.num_props = 1
        target.propid_list = [0]

        return target

    def select_next_targets(self) -> list[DriverTarget]:
        """Select a list of targets.

        By default it will just return a dummy test target.

        Returns
        -------
        `list` [`DriverTarget`]
        """

        return [self.select_next_target()]

    def register_observed_target(self, target: DriverTarget) -> Observation:
        """Validates observed target and returns an observation.

        Parameters
        ----------
        target : `DriverTarget`
            Observed target to register.

        Returns
        -------
        `Observation`
            Registered observation.
        """
        self.log.log(WORDY, "Registering target %s.", target)

        return target.get_observation()

    def register_observation(self, target: DriverTarget) -> None:
        """Register observations.

        This method should store the observation in a way that can be retrieved
        afterwards by the driver.

        The default implementation is not implemented.

        Parameters
        ----------
        target : `DriverTarget`
            Observation to register.
        """
        self.register_observed_target(target=target)

    def get_stop_tracking_target(self) -> DriverTarget:
        stop_tracking_block = observing.ObservingBlock(
            name="StopTracking",
            program="_Internal",
            constraints=[],
            scripts=[
                observing.ObservingScript(
                    name=self.stop_tracking_script_name,
                    standard=self.stop_tracking_script_is_standard,
                    parameters=dict(),
                )
            ],
        )
        target = DriverTarget(
            observing_block=stop_tracking_block,
            targetid=self.targetid,
        )

        return target

    def load(self, config: str) -> None:
        """Load a modifying configuration.

        The input is a file that the Driver must be able to parse. It should
        contain that the driver can parse to reconfigure the current scheduler
        algorithm. For instance, it could contain new targets to add to a queue
        or project.

        Each Driver must implement its own load method. This method just checks
        that the file exists.

        Parameters
        ----------
        config : `str`
            Configuration to load

        Raises
        ------
        RuntimeError:
            If input configuration file does not exists.

        """
        if not os.path.exists(config):
            raise RuntimeError(f"Input configuration file {config} does not exist.")

    def save_state(self) -> None:
        """Save the current state of the scheduling algorithm to a file.

        Returns
        -------
        filename: `str`
            The name of the file with the state.
        """
        raise NotImplementedError("Save state is is not implemented.")

    def parse_observation_database(self, filename: str) -> None:
        """Parse an observation database into a list of observations.

        Parameters
        ----------
        filename : `str`

        Returns
        -------
        observations : `list` of `DriverTarget`
        """
        raise NotImplementedError("Parse observation database not implemented.")

    def get_state_as_file_object(self) -> io.BytesIO:
        """Get the current state of the scheduling algorithm as a file object.

        Returns
        -------
        file_object : `io.BytesIO`
            File object with the current.
        """
        raise NotImplementedError("Get state as file object not implemented.")

    def reset_from_state(self, filename: str) -> None:
        """Load the state from a file."""
        raise NotImplementedError("Reset from state is not implemented.")

    def assert_survey_observing_script(self, survey_name: str) -> None:
        """Assert that the input survey name has a dedicated observing script.

        Parameters
        ----------
        survey_name : `str`
            Name of the survey.

        Raises
        ------
        AssertError:
            If `survey_name` is not in the list of valid survey observing
            scripts.
        """
        assert survey_name in self.observing_blocks, (
            f"{survey_name} not in the list of observing blocks. "
            f"Currently defined are: {set(self.observing_blocks.keys())}"
        )

    def get_survey_observing_block(self, survey_name: str) -> observing.ObservingBlock:
        """Return the appropriate survey observing block.

        Parameters
        ----------
        survey_name : `str`
            Name of the survey.

        Returns
        -------
        observing.ObservingBlock
            Survey observing block.
        """

        self.assert_survey_observing_script(survey_name=survey_name)

        return self.observing_blocks[survey_name].copy(deep=True)

    def convert_efd_observations_to_targets(
        self, efd_observations: pandas.DataFrame
    ) -> typing.List[DriverTarget]:
        """Convert EFD dataframe into list of driver targets.

        Parameters
        ----------
        efd_observations : `pandas.DataFrame`
            Data frame returned from a query to the EFD for observations.
        """

        observations = []

        for observation_data_frame in efd_observations.iterrows():
            observations.append(
                self._get_driver_target_from_observation_data_frame(
                    observation_data_frame=observation_data_frame
                )
            )

        return observations

    def _get_driver_target_from_observation_data_frame(
        self, observation_data_frame: typing.Tuple[pandas.Timestamp, pandas.Series]
    ) -> DriverTarget:
        """Convert an observation data frame into a DriverTarget.

        Parameters
        ----------
        observation_data_frame :  pandas.DataFrame
            An observation data frame.
        """

        return DriverTarget(
            observing_script_name=self.default_observing_script_name,
            observing_script_is_standard=self.default_observing_script_is_standard,
            targetid=observation_data_frame["targetId"],
        )

f546d59e8ae5aa969ee552c619f79870775ab97e
<mask token>


class Driver:
    <mask token>
    <mask token>

    def configure_scheduler(self, config: types.SimpleNamespace
        ) ->SurveyTopology:
        """This method is responsible for running the scheduler configuration
        and returning the survey topology, which specifies the number, name
        and type of projects running by the scheduler.

        By default it will just return a test survey topology.

        Parameters
        ----------
        config : `types.SimpleNamespace`
            Configuration, as described by ``schema/Scheduler.yaml``

        Returns
        -------
        survey_topology : ``SurveyTopology``
            Survey topology.
        """
        if 'parameters' in config.driver_configuration:
            for parameter in config.driver_configuration['parameters']:
                value = config.driver_configuration['parameters'][parameter]
                self.log.debug(
                    f'Setting driver parameter: {parameter} = {value}')
                setattr(self.parameters, parameter, value)
        if not hasattr(config, 'driver_configuration'):
            raise RuntimeError(
                'No driver_configuration section defined in configuration.')
        return self.get_survey_topology(config)
    <mask token>

    def cold_start(self, observations: list[Observation]) ->None:
        """Rebuilds the internal state of the scheduler from a list of
        observations.

        Parameters
        ----------
        observations : `list`[`Observation`]
            List of observations.
        """
        raise NotImplementedError('Cold start is not implemented.')
    <mask token>
    <mask token>

    def select_next_targets(self) ->list[DriverTarget]:
        """Select a list of targets.

        By default it will just return a dummy test target.

        Returns
        -------
        `list` [`DriverTarget`]
        """
        return [self.select_next_target()]
    <mask token>
    <mask token>
    <mask token>
    <mask token>
    <mask token>

    def parse_observation_database(self, filename: str) ->None:
        """Parse an observation database into a list of observations.

        Parameters
        ----------
        filename : `str`

        Returns
        -------
        observations : `list` of `DriverTarget`
        """
        raise NotImplementedError('Parse observation database not implemented.'
            )
    <mask token>

    def reset_from_state(self, filename: str) ->None:
        """Load the state from a file."""
        raise NotImplementedError('Reset from state is not implemented.')

    def assert_survey_observing_script(self, survey_name: str) ->None:
        """Assert that the input survey name has a dedicated observing script.

        Parameters
        ----------
        survey_name : `str`
            Name of the survey.

        Raises
        ------
        AssertError:
            If `survey_name` is not in the list of valid survey observing
            scripts.
        """
        assert survey_name in self.observing_blocks, f'{survey_name} not in the list of observing blocks. Currently defined are: {set(self.observing_blocks.keys())}'
    <mask token>

    def convert_efd_observations_to_targets(self, efd_observations: pandas.
        DataFrame) ->typing.List[DriverTarget]:
        """Convert EFD dataframe into list of driver targets.

        Parameters
        ----------
        efd_observations : `pandas.DataFrame`
            Data frame returned from a query to the EFD for observations.
        """
        observations = []
        for observation_data_frame in efd_observations.iterrows():
            observations.append(self.
                _get_driver_target_from_observation_data_frame(
                observation_data_frame=observation_data_frame))
        return observations

    def _get_driver_target_from_observation_data_frame(self,
        observation_data_frame: typing.Tuple[pandas.Timestamp, pandas.Series]
        ) ->DriverTarget:
        """Convert an observation data frame into a DriverTarget.

        Parameters
        ----------
        observation_data_frame :  pandas.DataFrame
            An observation data frame.
        """
        return DriverTarget(observing_script_name=self.
            default_observing_script_name, observing_script_is_standard=
            self.default_observing_script_is_standard, targetid=
            observation_data_frame['targetId'])

<mask token>


class Driver:
    <mask token>
    <mask token>

    def configure_scheduler(self, config: types.SimpleNamespace
        ) ->SurveyTopology:
        """This method is responsible for running the scheduler configuration
        and returning the survey topology, which specifies the number, name
        and type of projects running by the scheduler.

        By default it will just return a test survey topology.

        Parameters
        ----------
        config : `types.SimpleNamespace`
            Configuration, as described by ``schema/Scheduler.yaml``

        Returns
        -------
        survey_topology : ``SurveyTopology``
            Survey topology.
        """
        if 'parameters' in config.driver_configuration:
            for parameter in config.driver_configuration['parameters']:
                value = config.driver_configuration['parameters'][parameter]
                self.log.debug(
                    f'Setting driver parameter: {parameter} = {value}')
                setattr(self.parameters, parameter, value)
        if not hasattr(config, 'driver_configuration'):
            raise RuntimeError(
                'No driver_configuration section defined in configuration.')
        return self.get_survey_topology(config)
    <mask token>

    def cold_start(self, observations: list[Observation]) ->None:
        """Rebuilds the internal state of the scheduler from a list of
        observations.

        Parameters
        ----------
        observations : `list`[`Observation`]
            List of observations.
        """
        raise NotImplementedError('Cold start is not implemented.')

    def update_conditions(self) ->None:
        """Update driver internal conditions.

        When subclassing this method, make sure to call it at the start of the
        method, as it performs operations like running the observatory through
        the current targets on the queue.
        """
        self.log.debug('Updating conditions.')
        self.models['sky'].update(self.models['observatory_state'].time)
        if self.is_night is None:
            self.log.debug(
                'Driver not initialized yet. Computing night parameters.')
            self.current_sunset, self.current_sunrise = self.models['sky'
                ].get_night_boundaries(self.parameters.night_boundary)
            self.is_night = self.current_sunset <= self.models[
                'observatory_state'].time < self.current_sunrise
            self.log.debug(
                f'Sunset/Sunrise: {self.current_sunset}/{self.current_sunrise}, sun @ {self.parameters.night_boundary} degrees.'
                )
        is_night = self.is_night
        self.is_night = self.current_sunset <= self.models['observatory_state'
            ].time < self.current_sunrise
        if not self.is_night and is_night:
            self.log.debug(
                f'Night over. Computing next night boundaries. Assuming sun elevation of {self.parameters.night_boundary}.'
                )
            self.night += 1
            self.current_sunset, self.current_sunrise = self.models['sky'
                ].get_night_boundaries(self.parameters.night_boundary)
            self.log.debug(
                f'[{self.night}]: Sunset/Sunrise: {self.current_sunset}/{self.current_sunrise} '
                )
    <mask token>

    def select_next_targets(self) ->list[DriverTarget]:
        """Select a list of targets.

        By default it will just return a dummy test target.

        Returns
        -------
        `list` [`DriverTarget`]
        """
        return [self.select_next_target()]

    def register_observed_target(self, target: DriverTarget) ->Observation:
        """Validates observed target and returns an observation.

        Parameters
        ----------
        target : `DriverTarget`
            Observed target to register.

        Returns
        -------
        `Observation`
            Registered observation.
        """
        self.log.log(WORDY, 'Registering target %s.', target)
        return target.get_observation()

    def register_observation(self, target: DriverTarget) ->None:
        """Register observations.

        This method should store the observation in a way that can be retrieved
        afterwards by the driver.

        The default implementation is not implemented.

        Parameters
        ----------
        target : `DriverTarget`
            Observation to register.
        """
        self.register_observed_target(target=target)

    def get_stop_tracking_target(self) ->DriverTarget:
        stop_tracking_block = observing.ObservingBlock(name='StopTracking',
            program='_Internal', constraints=[], scripts=[observing.
            ObservingScript(name=self.stop_tracking_script_name, standard=
            self.stop_tracking_script_is_standard, parameters=dict())])
        target = DriverTarget(observing_block=stop_tracking_block, targetid
            =self.targetid)
        return target
    <mask token>
    <mask token>

    def parse_observation_database(self, filename: str) ->None:
        """Parse an observation database into a list of observations.

        Parameters
        ----------
        filename : `str`

        Returns
        -------
        observations : `list` of `DriverTarget`
        """
        raise NotImplementedError('Parse observation database not implemented.'
            )
    <mask token>

    def reset_from_state(self, filename: str) ->None:
        """Load the state from a file."""
        raise NotImplementedError('Reset from state is not implemented.')

    def assert_survey_observing_script(self, survey_name: str) ->None:
        """Assert that the input survey name has a dedicated observing script.

        Parameters
        ----------
        survey_name : `str`
            Name of the survey.

        Raises
        ------
        AssertError:
            If `survey_name` is not in the list of valid survey observing
            scripts.
        """
        assert survey_name in self.observing_blocks, f'{survey_name} not in the list of observing blocks. Currently defined are: {set(self.observing_blocks.keys())}'
    <mask token>

    def convert_efd_observations_to_targets(self, efd_observations: pandas.
        DataFrame) ->typing.List[DriverTarget]:
        """Convert EFD dataframe into list of driver targets.

        Parameters
        ----------
        efd_observations : `pandas.DataFrame`
            Data frame returned from a query to the EFD for observations.
        """
        observations = []
        for observation_data_frame in efd_observations.iterrows():
            observations.append(self.
                _get_driver_target_from_observation_data_frame(
                observation_data_frame=observation_data_frame))
        return observations

    def _get_driver_target_from_observation_data_frame(self,
        observation_data_frame: typing.Tuple[pandas.Timestamp, pandas.Series]
        ) ->DriverTarget:
        """Convert an observation data frame into a DriverTarget.

        Parameters
        ----------
        observation_data_frame :  pandas.DataFrame
            An observation data frame.
        """
        return DriverTarget(observing_script_name=self.
            default_observing_script_name, observing_script_is_standard=
            self.default_observing_script_is_standard, targetid=
            observation_data_frame['targetId'])

<mask token>


class Driver:
    <mask token>
    <mask token>

    def configure_scheduler(self, config: types.SimpleNamespace
        ) ->SurveyTopology:
        """This method is responsible for running the scheduler configuration
        and returning the survey topology, which specifies the number, name
        and type of projects running by the scheduler.

        By default it will just return a test survey topology.

        Parameters
        ----------
        config : `types.SimpleNamespace`
            Configuration, as described by ``schema/Scheduler.yaml``

        Returns
        -------
        survey_topology : ``SurveyTopology``
            Survey topology.
        """
        if 'parameters' in config.driver_configuration:
            for parameter in config.driver_configuration['parameters']:
                value = config.driver_configuration['parameters'][parameter]
                self.log.debug(
                    f'Setting driver parameter: {parameter} = {value}')
                setattr(self.parameters, parameter, value)
        if not hasattr(config, 'driver_configuration'):
            raise RuntimeError(
                'No driver_configuration section defined in configuration.')
        return self.get_survey_topology(config)

    def get_survey_topology(self, config: types.SimpleNamespace
        ) ->SurveyTopology:
        """Get the survey topology.

        Parameters
        ----------
        config : `types.SimpleNamespace`
            Configuration, as described by ``schema/Scheduler.yaml``

        Returns
        -------
        survey_topology : ``SurveyTopology``
            Survey topology.
        """
        survey_topology = SurveyTopology()
        survey_topology.general_propos = config.driver_configuration.get(
            'general_propos', [])
        survey_topology.sequence_propos = config.driver_configuration.get(
            'sequence_propos', [])
        survey_topology.num_general_props = len(survey_topology.general_propos)
        survey_topology.num_seq_props = len(survey_topology.sequence_propos)
        self.stop_tracking_script_name = config.driver_configuration[
            'stop_tracking_observing_script_name']
        self.stop_tracking_script_is_standard = config.driver_configuration[
            'stop_tracking_observing_script_is_standard']
        return survey_topology

    def cold_start(self, observations: list[Observation]) ->None:
        """Rebuilds the internal state of the scheduler from a list of
        observations.

        Parameters
        ----------
        observations : `list`[`Observation`]
            List of observations.
        """
        raise NotImplementedError('Cold start is not implemented.')

    def update_conditions(self) ->None:
        """Update driver internal conditions.

        When subclassing this method, make sure to call it at the start of the
        method, as it performs operations like running the observatory through
        the current targets on the queue.
        """
        self.log.debug('Updating conditions.')
        self.models['sky'].update(self.models['observatory_state'].time)
        if self.is_night is None:
            self.log.debug(
                'Driver not initialized yet. Computing night parameters.')
            self.current_sunset, self.current_sunrise = self.models['sky'
                ].get_night_boundaries(self.parameters.night_boundary)
            self.is_night = self.current_sunset <= self.models[
                'observatory_state'].time < self.current_sunrise
            self.log.debug(
                f'Sunset/Sunrise: {self.current_sunset}/{self.current_sunrise}, sun @ {self.parameters.night_boundary} degrees.'
                )
        is_night = self.is_night
        self.is_night = self.current_sunset <= self.models['observatory_state'
            ].time < self.current_sunrise
        if not self.is_night and is_night:
            self.log.debug(
                f'Night over. Computing next night boundaries. Assuming sun elevation of {self.parameters.night_boundary}.'
                )
            self.night += 1
            self.current_sunset, self.current_sunrise = self.models['sky'
                ].get_night_boundaries(self.parameters.night_boundary)
            self.log.debug(
                f'[{self.night}]: Sunset/Sunrise: {self.current_sunset}/{self.current_sunrise} '
                )
    <mask token>

    def select_next_targets(self) ->list[DriverTarget]:
        """Select a list of targets.

        By default it will just return a dummy test target.

        Returns
        -------
        `list` [`DriverTarget`]
        """
        return [self.select_next_target()]

    def register_observed_target(self, target: DriverTarget) ->Observation:
        """Validates observed target and returns an observation.

        Parameters
        ----------
        target : `DriverTarget`
            Observed target to register.

        Returns
        -------
        `Observation`
            Registered observation.
        """
        self.log.log(WORDY, 'Registering target %s.', target)
        return target.get_observation()

    def register_observation(self, target: DriverTarget) ->None:
        """Register observations.

        This method should store the observation in a way that can be retrieved
        afterwards by the driver.

        The default implementation is not implemented.

        Parameters
        ----------
        target : `DriverTarget`
            Observation to register.
        """
        self.register_observed_target(target=target)

    def get_stop_tracking_target(self) ->DriverTarget:
        stop_tracking_block = observing.ObservingBlock(name='StopTracking',
            program='_Internal', constraints=[], scripts=[observing.
            ObservingScript(name=self.stop_tracking_script_name, standard=
            self.stop_tracking_script_is_standard, parameters=dict())])
        target = DriverTarget(observing_block=stop_tracking_block, targetid
            =self.targetid)
        return target

    def load(self, config: str) ->None:
        """Load a modifying configuration.

        The input is a file that the Driver must be able to parse. It should
        contain that the driver can parse to reconfigure the current scheduler
        algorithm. For instance, it could contain new targets to add to a queue
        or project.

        Each Driver must implement its own load method. This method just checks
        that the file exists.

        Parameters
        ----------
        config : `str`
            Configuration to load

        Raises
        ------
        RuntimeError:
            If input configuration file does not exists.

        """
        if not os.path.exists(config):
            raise RuntimeError(
                f'Input configuration file {config} does not exist.')

    def save_state(self) ->None:
        """Save the current state of the scheduling algorithm to a file.

        Returns
        -------
        filename: `str`
            The name of the file with the state.
        """
        raise NotImplementedError('Save state is is not implemented.')

    def parse_observation_database(self, filename: str) ->None:
        """Parse an observation database into a list of observations.

        Parameters
        ----------
        filename : `str`

        Returns
        -------
        observations : `list` of `DriverTarget`
        """
        raise NotImplementedError('Parse observation database not implemented.'
            )

    def get_state_as_file_object(self) ->io.BytesIO:
        """Get the current state of the scheduling algorithm as a file object.

        Returns
        -------
        file_object : `io.BytesIO`
            File object with the current.
        """
        raise NotImplementedError('Get state as file object not implemented.')

    def reset_from_state(self, filename: str) ->None:
        """Load the state from a file."""
        raise NotImplementedError('Reset from state is not implemented.')

    def assert_survey_observing_script(self, survey_name: str) ->None:
        """Assert that the input survey name has a dedicated observing script.

        Parameters
        ----------
        survey_name : `str`
            Name of the survey.

        Raises
        ------
        AssertError:
            If `survey_name` is not in the list of valid survey observing
            scripts.
        """
        assert survey_name in self.observing_blocks, f'{survey_name} not in the list of observing blocks. Currently defined are: {set(self.observing_blocks.keys())}'

    def get_survey_observing_block(self, survey_name: str
        ) ->observing.ObservingBlock:
        """Return the appropriate survey observing block.

        Parameters
        ----------
        survey_name : `str`
            Name of the survey.

        Returns
        -------
        observing.ObservingBlock
            Survey observing block.
        """
        self.assert_survey_observing_script(survey_name=survey_name)
        return self.observing_blocks[survey_name].copy(deep=True)

    def convert_efd_observations_to_targets(self, efd_observations: pandas.
        DataFrame) ->typing.List[DriverTarget]:
        """Convert EFD dataframe into list of driver targets.

        Parameters
        ----------
        efd_observations : `pandas.DataFrame`
            Data frame returned from a query to the EFD for observations.
        """
        observations = []
        for observation_data_frame in efd_observations.iterrows():
            observations.append(self.
                _get_driver_target_from_observation_data_frame(
                observation_data_frame=observation_data_frame))
        return observations

    def _get_driver_target_from_observation_data_frame(self,
        observation_data_frame: typing.Tuple[pandas.Timestamp, pandas.Series]
        ) ->DriverTarget:
        """Convert an observation data frame into a DriverTarget.

        Parameters
        ----------
        observation_data_frame :  pandas.DataFrame
            An observation data frame.
        """
        return DriverTarget(observing_script_name=self.
            default_observing_script_name, observing_script_is_standard=
            self.default_observing_script_is_standard, targetid=
            observation_data_frame['targetId'])

<mask token>
__all__ = ['Driver', 'DriverParameters']
WORDY = logging.DEBUG - 5


@dataclass
class DriverParameters:
    """Actual global driver configuration parameters.

    This can be expanded for other scheduler drivers. For instance, if your
    scheduler uses a certain configuration file it is possible to subclass
    this and add the required parameters (e.g. file paths or else). Then,
    replace `self.params` on the Driver by the subclassed configuration.
    """
    night_boundary: float = -12.0
    new_moon_phase_threshold: float = 20.0

    def setDefaults(self) ->None:
        """Set defaults for the LSST Scheduler's Driver."""
        self.night_boundary = -12.0
        self.new_moon_phase_threshold = 20.0


class Driver:
    """The Scheduler Driver is the module that normalizes the interface between
    any scheduling algorithm to the LSST Scheduler CSC. The interface
    implements three main behaviours; configure an underlying algorithm,
    request targets and register successful observations.

    If the Scheduler algorithm requires a specific set of parameters the user
    must subclass `DriverParameters`, in the same module as the `Driver`, and
    add the appropriate parameters using the LSST pexConfig module.

    Access to the telemetry stream and models are also interfaced by `Driver`.
    The full list of default available telemetry data is shown in the
    scheduler_csc module. Nevertheless, the full list of telemetry may vary
    depending on the models used. The user has control over this while
    configuring the Scheduler CSC.

    Parameters
    ----------
    models: `dict`
        A dictionary with models available for the scheduling algorithm.
    raw_telemetry: `dict`
        A dictionary with available raw telemetry.
    observing_blocks : dict[str, observing.ObservingBlock]
        Observing blocks.
    parameters: `DriverParameters`, optional
        Input functional parameters for the scheduling algorithm.
    log: `Logger`
        Parent logger class.

    Attributes
    ----------
    log: `Logger`
        Logger class.
    parameters: `DriverParameters`
        Store functional driver parameters.
    models: `dict`
        Dictionary with models.
    raw_telemetry: `dict`
        Dictionary with raw telemetry data.
    target_id: `int`
        Counter of the number of targets generated by the scheduler since it
        was last instantiated.
    observing_blocks : dict[str, observing.ObservingBlock]
        Observing blocks.
    stop_tracking_script_name: `str`
        Name of a SAL Script that can be used to stop the observatory. This is,
        in general, queued by the Driver when it fails to select an
        observation.
    stop_tracking_script_is_standard: `bool`
        Specify if the stop tracking script is standard.
    is_night: `bool`
        Track if it is night time.
    night: `int`
        Count the number of nights from the start of the survey.
    current_sunset: `float`
        Timestamp for the current sunset.
    current_sunrise: `float`
        Timestamp for the current sunrise.
    """

    def __init__(self, models: dict[str, typing.Any], raw_telemetry: dict[
        str, typing.Any], observing_blocks: dict[str, observing.
        ObservingBlock], parameters: (DriverParameters | None)=None, log: (
        logging.Logger | None)=None) ->None:
        if log is None:
            self.log = logging.getLogger(type(self).__name__)
        else:
            self.log = log.getChild(type(self).__name__)
        if parameters is None:
            self.parameters = DriverParameters()
        else:
            self.parameters = parameters
        self.models = models
        self.raw_telemetry = raw_telemetry
        self.targetid = 0
        self.observing_blocks = observing_blocks
        self.stop_tracking_script_name = None
        self.stop_tracking_script_is_standard = None
        self.is_night = None
        self.night = 1
        self.current_sunset = None
        self.current_sunrise = None

    def configure_scheduler(self, config: types.SimpleNamespace
        ) ->SurveyTopology:
        """This method is responsible for running the scheduler configuration
        and returning the survey topology, which specifies the number, name
        and type of projects running by the scheduler.

        By default it will just return a test survey topology.

        Parameters
        ----------
        config : `types.SimpleNamespace`
            Configuration, as described by ``schema/Scheduler.yaml``

        Returns
        -------
        survey_topology : ``SurveyTopology``
            Survey topology.
        """
        if 'parameters' in config.driver_configuration:
            for parameter in config.driver_configuration['parameters']:
                value = config.driver_configuration['parameters'][parameter]
                self.log.debug(
                    f'Setting driver parameter: {parameter} = {value}')
                setattr(self.parameters, parameter, value)
        if not hasattr(config, 'driver_configuration'):
            raise RuntimeError(
                'No driver_configuration section defined in configuration.')
        return self.get_survey_topology(config)

    def get_survey_topology(self, config: types.SimpleNamespace
        ) ->SurveyTopology:
        """Get the survey topology.

        Parameters
        ----------
        config : `types.SimpleNamespace`
            Configuration, as described by ``schema/Scheduler.yaml``

        Returns
        -------
        survey_topology : ``SurveyTopology``
            Survey topology.
        """
        survey_topology = SurveyTopology()
        survey_topology.general_propos = config.driver_configuration.get(
            'general_propos', [])
        survey_topology.sequence_propos = config.driver_configuration.get(
            'sequence_propos', [])
        survey_topology.num_general_props = len(survey_topology.general_propos)
        survey_topology.num_seq_props = len(survey_topology.sequence_propos)
        self.stop_tracking_script_name = config.driver_configuration[
            'stop_tracking_observing_script_name']
        self.stop_tracking_script_is_standard = config.driver_configuration[
            'stop_tracking_observing_script_is_standard']
        return survey_topology

    def cold_start(self, observations: list[Observation]) ->None:
        """Rebuilds the internal state of the scheduler from a list of
        observations.

        Parameters
        ----------
        observations : `list`[`Observation`]
            List of observations.
        """
        raise NotImplementedError('Cold start is not implemented.')

    def update_conditions(self) ->None:
        """Update driver internal conditions.

        When subclassing this method, make sure to call it at the start of the
        method, as it performs operations like running the observatory through
        the current targets on the queue.
        """
        self.log.debug('Updating conditions.')
        self.models['sky'].update(self.models['observatory_state'].time)
        if self.is_night is None:
            self.log.debug(
                'Driver not initialized yet. Computing night parameters.')
            self.current_sunset, self.current_sunrise = self.models['sky'
                ].get_night_boundaries(self.parameters.night_boundary)
            self.is_night = self.current_sunset <= self.models[
                'observatory_state'].time < self.current_sunrise
            self.log.debug(
                f'Sunset/Sunrise: {self.current_sunset}/{self.current_sunrise}, sun @ {self.parameters.night_boundary} degrees.'
                )
        is_night = self.is_night
        self.is_night = self.current_sunset <= self.models['observatory_state'
            ].time < self.current_sunrise
        if not self.is_night and is_night:
            self.log.debug(
                f'Night over. Computing next night boundaries. Assuming sun elevation of {self.parameters.night_boundary}.'
                )
            self.night += 1
            self.current_sunset, self.current_sunrise = self.models['sky'
                ].get_night_boundaries(self.parameters.night_boundary)
            self.log.debug(
                f'[{self.night}]: Sunset/Sunrise: {self.current_sunset}/{self.current_sunrise} '
                )

    def select_next_target(self) ->DriverTarget:
        """Picks a target and returns it as a target object.

        By default it will just return a dummy test target.

        Returns
        -------
        `DriverTarget`
        """
        self.log.log(WORDY, 'Selecting next target.')
        self.targetid += 1
        target = DriverTarget(observing_block=next(iter(self.
            observing_blocks.values())), targetid=self.targetid)
        target.num_exp = 2
        target.exp_times = [15.0, 15.0]
        target.num_props = 1
        target.propid_list = [0]
        return target

    def select_next_targets(self) ->list[DriverTarget]:
        """Select a list of targets.

        By default it will just return a dummy test target.

        Returns
        -------
        `list` [`DriverTarget`]
        """
        return [self.select_next_target()]

    def register_observed_target(self, target: DriverTarget) ->Observation:
        """Validates observed target and returns an observation.

        Parameters
        ----------
        target : `DriverTarget`
            Observed target to register.

        Returns
        -------
        `Observation`
            Registered observation.
        """
        self.log.log(WORDY, 'Registering target %s.', target)
        return target.get_observation()

    def register_observation(self, target: DriverTarget) ->None:
        """Register observations.

        This method should store the observation in a way that can be retrieved
        afterwards by the driver.

        The default implementation is not implemented.

        Parameters
        ----------
        target : `DriverTarget`
            Observation to register.
        """
        self.register_observed_target(target=target)

    def get_stop_tracking_target(self) ->DriverTarget:
        stop_tracking_block = observing.ObservingBlock(name='StopTracking',
            program='_Internal', constraints=[], scripts=[observing.
            ObservingScript(name=self.stop_tracking_script_name, standard=
            self.stop_tracking_script_is_standard, parameters=dict())])
        target = DriverTarget(observing_block=stop_tracking_block, targetid
            =self.targetid)
        return target

    def load(self, config: str) ->None:
        """Load a modifying configuration.

        The input is a file that the Driver must be able to parse. It should
        contain that the driver can parse to reconfigure the current scheduler
        algorithm. For instance, it could contain new targets to add to a queue
        or project.

        Each Driver must implement its own load method. This method just checks
        that the file exists.

        Parameters
        ----------
        config : `str`
            Configuration to load

        Raises
        ------
        RuntimeError:
            If input configuration file does not exists.

        """
        if not os.path.exists(config):
            raise RuntimeError(
                f'Input configuration file {config} does not exist.')

    def save_state(self) ->None:
        """Save the current state of the scheduling algorithm to a file.

        Returns
        -------
        filename: `str`
            The name of the file with the state.
        """
        raise NotImplementedError('Save state is is not implemented.')

    def parse_observation_database(self, filename: str) ->None:
        """Parse an observation database into a list of observations.

        Parameters
        ----------
        filename : `str`

        Returns
        -------
        observations : `list` of `DriverTarget`
        """
        raise NotImplementedError('Parse observation database not implemented.'
            )

    def get_state_as_file_object(self) ->io.BytesIO:
        """Get the current state of the scheduling algorithm as a file object.

        Returns
        -------
        file_object : `io.BytesIO`
            File object with the current.
        """
        raise NotImplementedError('Get state as file object not implemented.')

    def reset_from_state(self, filename: str) ->None:
        """Load the state from a file."""
        raise NotImplementedError('Reset from state is not implemented.')

    def assert_survey_observing_script(self, survey_name: str) ->None:
        """Assert that the input survey name has a dedicated observing script.

        Parameters
        ----------
        survey_name : `str`
            Name of the survey.

        Raises
        ------
        AssertError:
            If `survey_name` is not in the list of valid survey observing
            scripts.
        """
        assert survey_name in self.observing_blocks, f'{survey_name} not in the list of observing blocks. Currently defined are: {set(self.observing_blocks.keys())}'

    def get_survey_observing_block(self, survey_name: str
        ) ->observing.ObservingBlock:
        """Return the appropriate survey observing block.

        Parameters
        ----------
        survey_name : `str`
            Name of the survey.

        Returns
        -------
        observing.ObservingBlock
            Survey observing block.
        """
        self.assert_survey_observing_script(survey_name=survey_name)
        return self.observing_blocks[survey_name].copy(deep=True)

    def convert_efd_observations_to_targets(self, efd_observations: pandas.
        DataFrame) ->typing.List[DriverTarget]:
        """Convert EFD dataframe into list of driver targets.

        Parameters
        ----------
        efd_observations : `pandas.DataFrame`
            Data frame returned from a query to the EFD for observations.
        """
        observations = []
        for observation_data_frame in efd_observations.iterrows():
            observations.append(self.
                _get_driver_target_from_observation_data_frame(
                observation_data_frame=observation_data_frame))
        return observations

    def _get_driver_target_from_observation_data_frame(self,
        observation_data_frame: typing.Tuple[pandas.Timestamp, pandas.Series]
        ) ->DriverTarget:
        """Convert an observation data frame into a DriverTarget.

        Parameters
        ----------
        observation_data_frame :  pandas.DataFrame
            An observation data frame.
        """
        return DriverTarget(observing_script_name=self.
            default_observing_script_name, observing_script_is_standard=
            self.default_observing_script_is_standard, targetid=
            observation_data_frame['targetId'])

# This file is part of ts_scheduler
#
# Developed for the LSST Telescope and Site Systems.
# This product includes software developed by the LSST Project
# (https://www.lsst.org).
# See the COPYRIGHT file at the top-level directory of this distribution
# for details of code ownership.
#
# 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 3 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

import io
import logging
import os
import types
import typing
from dataclasses import dataclass

import pandas
from lsst.ts import observing

from .driver_target import DriverTarget
from .observation import Observation
from .survey_topology import SurveyTopology

__all__ = ["Driver", "DriverParameters"]

WORDY = logging.DEBUG - 5


@dataclass
class DriverParameters:
    """Actual global driver configuration parameters.

    This can be expanded for other scheduler drivers. For instance, if your
    scheduler uses a certain configuration file it is possible to subclass
    this and add the required parameters (e.g. file paths or else). Then,
    replace `self.params` on the Driver by the subclassed configuration.
    """

    night_boundary: float = -12.0
    new_moon_phase_threshold: float = 20.0

    def setDefaults(self) -> None:
        """Set defaults for the LSST Scheduler's Driver."""
        self.night_boundary = -12.0
        self.new_moon_phase_threshold = 20.0


class Driver:
    """The Scheduler Driver is the module that normalizes the interface between
    any scheduling algorithm to the LSST Scheduler CSC. The interface
    implements three main behaviours; configure an underlying algorithm,
    request targets and register successful observations.

    If the Scheduler algorithm requires a specific set of parameters the user
    must subclass `DriverParameters`, in the same module as the `Driver`, and
    add the appropriate parameters using the LSST pexConfig module.

    Access to the telemetry stream and models are also interfaced by `Driver`.
    The full list of default available telemetry data is shown in the
    scheduler_csc module. Nevertheless, the full list of telemetry may vary
    depending on the models used. The user has control over this while
    configuring the Scheduler CSC.

    Parameters
    ----------
    models: `dict`
        A dictionary with models available for the scheduling algorithm.
    raw_telemetry: `dict`
        A dictionary with available raw telemetry.
    observing_blocks : dict[str, observing.ObservingBlock]
        Observing blocks.
    parameters: `DriverParameters`, optional
        Input functional parameters for the scheduling algorithm.
    log: `Logger`
        Parent logger class.

    Attributes
    ----------
    log: `Logger`
        Logger class.
    parameters: `DriverParameters`
        Store functional driver parameters.
    models: `dict`
        Dictionary with models.
    raw_telemetry: `dict`
        Dictionary with raw telemetry data.
    target_id: `int`
        Counter of the number of targets generated by the scheduler since it
        was last instantiated.
    observing_blocks : dict[str, observing.ObservingBlock]
        Observing blocks.
    stop_tracking_script_name: `str`
        Name of a SAL Script that can be used to stop the observatory. This is,
        in general, queued by the Driver when it fails to select an
        observation.
    stop_tracking_script_is_standard: `bool`
        Specify if the stop tracking script is standard.
    is_night: `bool`
        Track if it is night time.
    night: `int`
        Count the number of nights from the start of the survey.
    current_sunset: `float`
        Timestamp for the current sunset.
    current_sunrise: `float`
        Timestamp for the current sunrise.
    """

    def __init__(
        self,
        models: dict[str, typing.Any],
        raw_telemetry: dict[str, typing.Any],
        observing_blocks: dict[str, observing.ObservingBlock],
        parameters: DriverParameters | None = None,
        log: logging.Logger | None = None,
    ) -> None:
        if log is None:
            self.log = logging.getLogger(type(self).__name__)
        else:
            self.log = log.getChild(type(self).__name__)

        if parameters is None:
            self.parameters = DriverParameters()
        else:
            self.parameters = parameters
        self.models = models
        self.raw_telemetry = raw_telemetry
        self.targetid = 0

        self.observing_blocks = observing_blocks

        self.stop_tracking_script_name = None
        self.stop_tracking_script_is_standard = None

        self.is_night = None
        self.night = 1
        self.current_sunset = None
        self.current_sunrise = None

    def configure_scheduler(self, config: types.SimpleNamespace) -> SurveyTopology:
        """This method is responsible for running the scheduler configuration
        and returning the survey topology, which specifies the number, name
        and type of projects running by the scheduler.

        By default it will just return a test survey topology.

        Parameters
        ----------
        config : `types.SimpleNamespace`
            Configuration, as described by ``schema/Scheduler.yaml``

        Returns
        -------
        survey_topology : ``SurveyTopology``
            Survey topology.
        """

        if "parameters" in config.driver_configuration:
            for parameter in config.driver_configuration["parameters"]:
                value = config.driver_configuration["parameters"][parameter]
                self.log.debug(f"Setting driver parameter: {parameter} = {value}")
                setattr(
                    self.parameters,
                    parameter,
                    value,
                )

        if not hasattr(config, "driver_configuration"):
            raise RuntimeError(
                "No driver_configuration section defined in configuration."
            )

        return self.get_survey_topology(config)

    def get_survey_topology(self, config: types.SimpleNamespace) -> SurveyTopology:
        """Get the survey topology.

        Parameters
        ----------
        config : `types.SimpleNamespace`
            Configuration, as described by ``schema/Scheduler.yaml``

        Returns
        -------
        survey_topology : ``SurveyTopology``
            Survey topology.
        """
        survey_topology = SurveyTopology()

        survey_topology.general_propos = config.driver_configuration.get(
            "general_propos", []
        )
        survey_topology.sequence_propos = config.driver_configuration.get(
            "sequence_propos", []
        )
        survey_topology.num_general_props = len(survey_topology.general_propos)
        survey_topology.num_seq_props = len(survey_topology.sequence_propos)

        self.stop_tracking_script_name = config.driver_configuration[
            "stop_tracking_observing_script_name"
        ]
        self.stop_tracking_script_is_standard = config.driver_configuration[
            "stop_tracking_observing_script_is_standard"
        ]

        return survey_topology

    def cold_start(self, observations: list[Observation]) -> None:
        """Rebuilds the internal state of the scheduler from a list of
        observations.

        Parameters
        ----------
        observations : `list`[`Observation`]
            List of observations.
        """
        raise NotImplementedError("Cold start is not implemented.")

    def update_conditions(self) -> None:
        """Update driver internal conditions.

        When subclassing this method, make sure to call it at the start of the
        method, as it performs operations like running the observatory through
        the current targets on the queue.
        """
        self.log.debug("Updating conditions.")

        self.models["sky"].update(self.models["observatory_state"].time)

        if self.is_night is None:
            self.log.debug("Driver not initialized yet. Computing night parameters.")
            # Driver was not initialized yet. Need to compute night
            # boundaries

            (self.current_sunset, self.current_sunrise) = self.models[
                "sky"
            ].get_night_boundaries(self.parameters.night_boundary)

            self.is_night = (
                self.current_sunset
                <= self.models["observatory_state"].time
                < self.current_sunrise
            )

            self.log.debug(
                f"Sunset/Sunrise: {self.current_sunset}/{self.current_sunrise}, "
                f"sun @ {self.parameters.night_boundary} degrees."
            )

        is_night = self.is_night

        self.is_night = (
            self.current_sunset
            <= self.models["observatory_state"].time
            < self.current_sunrise
        )

        # Only compute night boundaries when we transition from nighttime to
        # daytime. Possibilities are:
        # 1 - self.is_night=True and is_night = True: During the night (no need
        #     to compute anything).
        # 2 - self.is_night=False and is_night = True: Transitioned from
        #     night/day (need to recompute night boundaries).
        # 3 - self.is_night=True and is_night = False: Transitioned from
        #     day/night (no need to compute anything).
        # 4 - self.is_night=False and is_night = False: During the day, no need
        #     to compute anything.
        if not self.is_night and is_night:
            self.log.debug(
                "Night over. Computing next night boundaries. "
                f"Assuming sun elevation of {self.parameters.night_boundary}."
            )
            self.night += 1
            (self.current_sunset, self.current_sunrise) = self.models[
                "sky"
            ].get_night_boundaries(self.parameters.night_boundary)

            self.log.debug(
                f"[{self.night}]: Sunset/Sunrise: {self.current_sunset}/{self.current_sunrise} "
            )

    def select_next_target(self) -> DriverTarget:
        """Picks a target and returns it as a target object.

        By default it will just return a dummy test target.

        Returns
        -------
        `DriverTarget`
        """
        self.log.log(WORDY, "Selecting next target.")

        self.targetid += 1

        target = DriverTarget(
            observing_block=next(iter(self.observing_blocks.values())),
            targetid=self.targetid,
        )

        target.num_exp = 2
        target.exp_times = [15.0, 15.0]
        target.num_props = 1
        target.propid_list = [0]

        return target

    def select_next_targets(self) -> list[DriverTarget]:
        """Select a list of targets.

        By default it will just return a dummy test target.

        Returns
        -------
        `list` [`DriverTarget`]
        """

        return [self.select_next_target()]

    def register_observed_target(self, target: DriverTarget) -> Observation:
        """Validates observed target and returns an observation.

        Parameters
        ----------
        target : `DriverTarget`
            Observed target to register.

        Returns
        -------
        `Observation`
            Registered observation.
        """
        self.log.log(WORDY, "Registering target %s.", target)

        return target.get_observation()

    def register_observation(self, target: DriverTarget) -> None:
        """Register observations.

        This method should store the observation in a way that can be retrieved
        afterwards by the driver.

        The default implementation is not implemented.

        Parameters
        ----------
        target : `DriverTarget`
            Observation to register.
        """
        self.register_observed_target(target=target)

    def get_stop_tracking_target(self) -> DriverTarget:
        stop_tracking_block = observing.ObservingBlock(
            name="StopTracking",
            program="_Internal",
            constraints=[],
            scripts=[
                observing.ObservingScript(
                    name=self.stop_tracking_script_name,
                    standard=self.stop_tracking_script_is_standard,
                    parameters=dict(),
                )
            ],
        )
        target = DriverTarget(
            observing_block=stop_tracking_block,
            targetid=self.targetid,
        )

        return target

    def load(self, config: str) -> None:
        """Load a modifying configuration.

        The input is a file that the Driver must be able to parse. It should
        contain that the driver can parse to reconfigure the current scheduler
        algorithm. For instance, it could contain new targets to add to a queue
        or project.

        Each Driver must implement its own load method. This method just checks
        that the file exists.

        Parameters
        ----------
        config : `str`
            Configuration to load

        Raises
        ------
        RuntimeError:
            If input configuration file does not exists.

        """
        if not os.path.exists(config):
            raise RuntimeError(f"Input configuration file {config} does not exist.")

    def save_state(self) -> None:
        """Save the current state of the scheduling algorithm to a file.

        Returns
        -------
        filename: `str`
            The name of the file with the state.
        """
        raise NotImplementedError("Save state is is not implemented.")

    def parse_observation_database(self, filename: str) -> None:
        """Parse an observation database into a list of observations.

        Parameters
        ----------
        filename : `str`

        Returns
        -------
        observations : `list` of `DriverTarget`
        """
        raise NotImplementedError("Parse observation database not implemented.")

    def get_state_as_file_object(self) -> io.BytesIO:
        """Get the current state of the scheduling algorithm as a file object.

        Returns
        -------
        file_object : `io.BytesIO`
            File object with the current.
        """
        raise NotImplementedError("Get state as file object not implemented.")

    def reset_from_state(self, filename: str) -> None:
        """Load the state from a file."""
        raise NotImplementedError("Reset from state is not implemented.")

    def assert_survey_observing_script(self, survey_name: str) -> None:
        """Assert that the input survey name has a dedicated observing script.

        Parameters
        ----------
        survey_name : `str`
            Name of the survey.

        Raises
        ------
        AssertError:
            If `survey_name` is not in the list of valid survey observing
            scripts.
        """
        assert survey_name in self.observing_blocks, (
            f"{survey_name} not in the list of observing blocks. "
            f"Currently defined are: {set(self.observing_blocks.keys())}"
        )

    def get_survey_observing_block(self, survey_name: str) -> observing.ObservingBlock:
        """Return the appropriate survey observing block.

        Parameters
        ----------
        survey_name : `str`
            Name of the survey.

        Returns
        -------
        observing.ObservingBlock
            Survey observing block.
        """

        self.assert_survey_observing_script(survey_name=survey_name)

        return self.observing_blocks[survey_name].copy(deep=True)

    def convert_efd_observations_to_targets(
        self, efd_observations: pandas.DataFrame
    ) -> typing.List[DriverTarget]:
        """Convert EFD dataframe into list of driver targets.

        Parameters
        ----------
        efd_observations : `pandas.DataFrame`
            Data frame returned from a query to the EFD for observations.
        """

        observations = []

        for observation_data_frame in efd_observations.iterrows():
            observations.append(
                self._get_driver_target_from_observation_data_frame(
                    observation_data_frame=observation_data_frame
                )
            )

        return observations

    def _get_driver_target_from_observation_data_frame(
        self, observation_data_frame: typing.Tuple[pandas.Timestamp, pandas.Series]
    ) -> DriverTarget:
        """Convert an observation data frame into a DriverTarget.

        Parameters
        ----------
        observation_data_frame :  pandas.DataFrame
            An observation data frame.
        """

        return DriverTarget(
            observing_script_name=self.default_observing_script_name,
            observing_script_is_standard=self.default_observing_script_is_standard,
            targetid=observation_data_frame["targetId"],
        )
