// commradio.cxx -- class to manage a nav radio instance
//
// Written by Torsten Dreyer, February 2014
//
// Copyright (C) 2000 - 2011  Curtis L. Olson - http://www.flightgear.org/~curt
//
// 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
//

#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif

#include "commradio.hxx"

#include <assert.h>

#include <simgear/sg_inlines.h>
#include <simgear/props/propertyObject.hxx>
#include <simgear/misc/strutils.hxx>

#include <ATC/CommStation.hxx>
#include <ATC/MetarPropertiesATISInformationProvider.hxx>
#include <ATC/CurrentWeatherATISInformationProvider.hxx>
#include <Airports/airport.hxx>
#include <Main/fg_props.hxx>
#include <Navaids/navlist.hxx>

#include <Sound/soundmanager.hxx>
#include <simgear/sound/sample_group.hxx>
#include <Sound/VoiceSynthesizer.hxx>

#include "frequencyformatter.hxx"

namespace Instrumentation {

using simgear::PropertyObject;
using std::string;

class AtisSpeaker: public SGPropertyChangeListener, SoundSampleReadyListener {
public:
  AtisSpeaker();
  virtual ~AtisSpeaker();
  virtual void valueChanged(SGPropertyNode * node);
  virtual void SoundSampleReady(SGSharedPtr<SGSoundSample>);

  bool hasSpokenAtis()
  {
    return _spokenAtis.empty() == false;
  }

  SGSharedPtr<SGSoundSample> getSpokenAtis()
  {
    return _spokenAtis.pop();
  }

  void setStationId(const string & stationId)
  {
    _stationId = stationId;
  }


private:
  SynthesizeRequest _synthesizeRequest;
  SGLockedQueue<SGSharedPtr<SGSoundSample> > _spokenAtis;
  string _stationId;
};

AtisSpeaker::AtisSpeaker()
{
  _synthesizeRequest.listener = this;
}

AtisSpeaker::~AtisSpeaker()
{

}
void AtisSpeaker::valueChanged(SGPropertyNode * node)
{
  using namespace simgear::strutils;

  if (!fgGetBool("/sim/sound/working", false))
    return;

  string newText = node->getStringValue();
  if (_synthesizeRequest.text == newText) return;

  _synthesizeRequest.text = newText;

  string voice = "cmu_us_arctic_slt";

  if (!_stationId.empty()) {
    // lets play a bit with the voice so not every airports atis sounds alike
    // but every atis of an airport has the same voice

    // create a simple hash from the last two letters of the airport's id
    unsigned char hash = 0;
    string::iterator i = _stationId.end() - 1;
    hash += *i;

    if( i != _stationId.begin() ) {
      --i;
      hash += *i;
    }

    _synthesizeRequest.speed = (hash % 16) / 16.0;
    _synthesizeRequest.pitch = (hash % 16) / 16.0;

    if( starts_with( _stationId, "K" ) || starts_with( _stationId, "C" ) ||
            starts_with( _stationId, "P" ) ) {
        voice = FLITEVoiceSynthesizer::getVoicePath("cmu_us_arctic_slt");
    } else if ( starts_with( _stationId, "EG" ) ) {
        voice = FLITEVoiceSynthesizer::getVoicePath("cstr_uk_female");
    } else {
        // Pick a random voice from the available voices
        voice = FLITEVoiceSynthesizer::getVoicePath(
            static_cast<FLITEVoiceSynthesizer::voice_t>(hash % FLITEVoiceSynthesizer::VOICE_UNKNOWN) );
    }
  }

  FGSoundManager * smgr = globals->get_subsystem<FGSoundManager>();
  if (!smgr) {
      return;
  }

  SG_LOG(SG_INSTR, SG_DEBUG,"AtisSpeaker voice is " << voice );
  FLITEVoiceSynthesizer * synthesizer = dynamic_cast<FLITEVoiceSynthesizer*>(smgr->getSynthesizer(voice));

  synthesizer->synthesize(_synthesizeRequest);
}

void AtisSpeaker::SoundSampleReady(SGSharedPtr<SGSoundSample> sample)
{
  // we are now in the synthesizers worker thread!
  _spokenAtis.push(sample);
}

SignalQualityComputer::~SignalQualityComputer()
{
}

class SimpleDistanceSquareSignalQualityComputer: public SignalQualityComputer {
public:
  SimpleDistanceSquareSignalQualityComputer()
      : _altitudeAgl_ft(fgGetNode("/position/altitude-agl-ft", true))
  {
  }

  ~SimpleDistanceSquareSignalQualityComputer()
  {
  }

  double computeSignalQuality(double distance_nm) const
  {
    // Very simple line of sight propagation model. It's cheap but it does the trick for now.
    // assume transmitter and receiver antennas are at some elevation above ground
    // so we have at least a range of 5NM. Add the approx. distance to the horizon.
    double range_nm = 5.0 + 1.23 * ::sqrt(SGMiscd::max(.0, _altitudeAgl_ft));
    return distance_nm < range_nm ? 1.0 : (range_nm * range_nm / distance_nm / distance_nm);
  }

private:
  PropertyObject<double> _altitudeAgl_ft;
};

class OnExitHandler {
public:
  virtual void onExit() = 0;
  virtual ~OnExitHandler()
  {
  }
};

class OnExit {
public:
  OnExit(OnExitHandler * onExitHandler)
      : _onExitHandler(onExitHandler)
  {
  }
  ~OnExit()
  {
    _onExitHandler->onExit();
  }
private:
  OnExitHandler * _onExitHandler;
};

class OutputProperties: public OnExitHandler {
public:

  void bind(SGPropertyNode* rn)
  {
      _rootNode = rn;

      _PO_stationType = PropertyObject<string>(_rootNode->getNode("station-type", true));
      _PO_stationName = PropertyObject<string>(_rootNode->getNode("station-name", true));
      _PO_airportId = PropertyObject<string>(_rootNode->getNode("airport-id", true));
      _PO_signalQuality_norm = PropertyObject<double>(_rootNode->getNode("signal-quality-norm", true));
      _PO_slantDistance_m = PropertyObject<double>(_rootNode->getNode("slant-distance-m", true));
      _PO_trueBearingTo_deg = PropertyObject<double>(_rootNode->getNode("true-bearing-to-deg", true));
      _PO_trueBearingFrom_deg = PropertyObject<double>(_rootNode->getNode("true-bearing-from-deg", true));
      _PO_trackDistance_m = PropertyObject<double>(_rootNode->getNode("track-distance-m", true));
      _PO_heightAboveStation_ft = PropertyObject<double>(_rootNode->getNode("height-above-station-ft", true));
  }

  virtual ~OutputProperties()
  {
  }

protected:
  SGPropertyNode_ptr _rootNode;

  std::string _stationType;
  std::string _stationName;
  std::string _airportId;
  double _signalQuality_norm = 0.0;
  double _slantDistance_m = 0.0;
  double _trueBearingTo_deg = 0.0;
  double _trueBearingFrom_deg = 0.0;
  double _trackDistance_m = 0.0;
  double _heightAboveStation_ft = 0.0;

private:
  PropertyObject<string> _PO_stationType;
  PropertyObject<string> _PO_stationName;
  PropertyObject<string> _PO_airportId;
  PropertyObject<double> _PO_signalQuality_norm;
  PropertyObject<double> _PO_slantDistance_m;
  PropertyObject<double> _PO_trueBearingTo_deg;
  PropertyObject<double> _PO_trueBearingFrom_deg;
  PropertyObject<double> _PO_trackDistance_m;
  PropertyObject<double> _PO_heightAboveStation_ft;

  virtual void onExit()
  {
    _PO_stationType = _stationType;
    _PO_stationName = _stationName;
    _PO_airportId = _airportId;
    _PO_signalQuality_norm = _signalQuality_norm;
    _PO_slantDistance_m = _slantDistance_m;
    _PO_trueBearingTo_deg = _trueBearingTo_deg;
    _PO_trueBearingFrom_deg = _trueBearingFrom_deg;
    _PO_trackDistance_m = _trackDistance_m;
    _PO_heightAboveStation_ft = _heightAboveStation_ft;
  }
};

/* ------------- The CommRadio implementation ---------------------- */

class MetarBridge: public SGReferenced, public SGPropertyChangeListener {
public:

  void bind();
  void unbind();
  void requestMetarForId(std::string & id);
  void clearMetar();
  void setMetarPropertiesRoot(SGPropertyNode_ptr n)
  {
    _metarPropertiesNode = n;
  }
  void setAtisNode(SGPropertyNode * n)
  {
    _atisNode = n;
  }

protected:
  virtual void valueChanged(SGPropertyNode *);

private:
  std::string _requestedId;
  SGPropertyNode_ptr _realWxEnabledNode;
  SGPropertyNode_ptr _metarPropertiesNode;
    SGPropertyNode * _atisNode = nullptr;
  ATISEncoder _atisEncoder;
};

typedef SGSharedPtr<MetarBridge> MetarBridgeRef;

void MetarBridge::bind()
{
  _realWxEnabledNode = fgGetNode("/environment/realwx/enabled", true);
  _metarPropertiesNode->getNode("valid", true)->addChangeListener(this);
}

void MetarBridge::unbind()
{
  _metarPropertiesNode->getNode("valid", true)->removeChangeListener(this);
}

void MetarBridge::requestMetarForId(std::string & id)
{
  std::string uppercaseId = simgear::strutils::uppercase(id);
  if (_requestedId == uppercaseId) return;
  _requestedId = uppercaseId;

  if (_realWxEnabledNode->getBoolValue()) {
    //  trigger a METAR request for the associated metarproperties
    _metarPropertiesNode->getNode("station-id", true)->setStringValue(uppercaseId);
    _metarPropertiesNode->getNode("valid", true)->setBoolValue(false);
    _metarPropertiesNode->getNode("time-to-live", true)->setDoubleValue(0.0);
  } else {
    // use the present weather to generate the ATIS.
    if ( NULL != _atisNode && false == _requestedId.empty()) {
      CurrentWeatherATISInformationProvider provider(_requestedId);
      _atisNode->setStringValue(_atisEncoder.encodeATIS(&provider));
    }
  }
}

void MetarBridge::clearMetar()
{
  string empty;
  requestMetarForId(empty);
}

void MetarBridge::valueChanged(SGPropertyNode * node)
{
  // check for raising edge of valid flag
  if ( NULL == node || false == node->getBoolValue() || false == _realWxEnabledNode->getBoolValue()) return;

  std::string responseId = simgear::strutils::uppercase(_metarPropertiesNode->getNode("station-id", true)->getStringValue());

  // unrequested metar!?
  if (responseId != _requestedId) return;

  if ( NULL != _atisNode) {
    MetarPropertiesATISInformationProvider provider(_metarPropertiesNode);
    _atisNode->setStringValue(_atisEncoder.encodeATIS(&provider));
  }
}

/* ------------- 8.3kHz Channel implementation ---------------------- */

class EightPointThreeFrequencyFormatter :
		public FrequencyFormatterBase,
		public SGPropertyChangeListener {
public:
	EightPointThreeFrequencyFormatter( SGPropertyNode_ptr root,
			const char * channel,
			const char * fmt,
			const char * width,
			const char * frq,
			const char * cnum ) :
		_channel( root, channel ),
		_frequency( root, frq ),
		_channelSpacing( root, width ),
		_formattedChannel( root, fmt ),
		_channelNum( root, cnum )
	{
		// ensure properties exist.
		_channel.node(true);
		_frequency.node(true);
		_channelSpacing.node(true);
		_channelNum.node(true);
		_formattedChannel.node(true);

		_channel.node()->addChangeListener( this, true );
		_channelNum.node()->addChangeListener( this, true );
	}

	virtual ~EightPointThreeFrequencyFormatter()
	{
		  _channel.node()->removeChangeListener( this );
		  _channelNum.node()->removeChangeListener( this );
	}

private:
	EightPointThreeFrequencyFormatter( const EightPointThreeFrequencyFormatter & );
	EightPointThreeFrequencyFormatter & operator = ( const EightPointThreeFrequencyFormatter & );

	void valueChanged (SGPropertyNode * prop) {
		if( prop == _channel.node() )
			setFrequency(prop->getDoubleValue());
		else if( prop == _channelNum.node() )
			setChannel(prop->getIntValue());
	}

	void setChannel( int channel ) {
		channel %= 3040;
		if( channel < 0 ) channel += 3040;
		double f = 118.000 + 0.025*(channel/4) + 0.005*(channel%4);
		if( f != _channel )	_channel = f;
	}

	void setFrequency( double channel ) {
		// format as fixed decimal "nnn.nnn"
		std::ostringstream buf;
		buf << std::fixed
		    << std::setw(6)
		    << std::setfill('0')
		    << std::setprecision(3)
		    << _channel;
		_formattedChannel = buf.str();

		// sanitize range and round to nearest kHz.
		unsigned c = static_cast<int>(SGMiscd::round(SGMiscd::clip( channel, 118.0, 136.99 ) * 1000));

		if ( (c % 25)  == 0 ) {
			// legacy 25kHz channels continue to be just that.
			_channelSpacing = 25.0;
			_frequency = c / 1000.0;

			int channelNum = (c-118000)/25*4;
			if( channelNum != _channelNum ) _channelNum = channelNum;

			if( _frequency != channel ) {
                const double channelValue = c / 1000.0;
                _channel = channelValue;
            }
		} else {
			_channelSpacing = 8.33;

			// 25kHz base frequency: xxx.000, xxx.025, xxx.050, xxx.075
			unsigned base25 = (c/25) * 25;

			// add n*8.33 to the 25kHz frequency
			unsigned subChannel = SGMisc<unsigned>::clip((c - base25)/5-1, 0, 2 );

			_frequency = (base25 + 8.33 * subChannel)/1000.0;

			int channelNum = (base25-118000)/25*4 + subChannel+1;
			if( channelNum != _channelNum ) _channelNum = channelNum;

			// set to correct channel on bogous input
			double sanitizedChannel = (base25 + 5*(subChannel+1))/1000.0;
			if( sanitizedChannel != channel ) {
				_channel = sanitizedChannel; // triggers recursion
			}
		}
	}

	double getFrequency() const {
		return _channel;
	}


    PropertyObject<double> _channel;
    PropertyObject<double> _frequency;
    PropertyObject<double> _channelSpacing;
    PropertyObject<string> _formattedChannel;
    PropertyObject<int>   _channelNum;

};


/* ------------- The CommRadio implementation ---------------------- */

class CommRadioImpl: public CommRadio,
                     OutputProperties
{
public:
    CommRadioImpl(SGPropertyNode_ptr node);
    virtual ~CommRadioImpl();

    // Subsystem API.
    void bind() override;
    void init() override;
    void unbind() override;
    void update(double dt) override;

private:
    bool _useEightPointThree = false;
    MetarBridgeRef _metarBridge;
    AtisSpeaker _atisSpeaker;
    SGSharedPtr<FrequencyFormatterBase> _useFrequencyFormatter;
    SGSharedPtr<FrequencyFormatterBase> _stbyFrequencyFormatter;
    const SignalQualityComputerRef _signalQualityComputer;

    double _stationTTL = 0.0;
    double _frequency = -1.0;
    flightgear::CommStationRef _commStationForFrequency;

    PropertyObject<double> _volume_norm;
    PropertyObject<string> _atis;
    PropertyObject<bool> _addNoise;
    PropertyObject<double> _cutoffSignalQuality;

    std::string _soundPrefix;
    void stopAudio();
    void updateAudio();

    SGSampleGroup* _sampleGroup = nullptr;
};

CommRadioImpl::CommRadioImpl(SGPropertyNode_ptr node) :
  _metarBridge(new MetarBridge),
  _signalQualityComputer(new SimpleDistanceSquareSignalQualityComputer)
{
  // set a special value to indicate we don't require a power supply node
  // by default
  setDefaultPowerSupplyPath("NO_DEFAULT");
  readConfig(node, "comm");
  _soundPrefix = name() + "_" + std::to_string(number()) + "_";
  _useEightPointThree = node->getBoolValue("eight-point-three", false );
}

CommRadioImpl::~CommRadioImpl()
{
}

void CommRadioImpl::bind()
{
  SGPropertyNode_ptr n = fgGetNode(nodePath(), true);
  OutputProperties::bind(n);

  _volume_norm = PropertyObject<double>(_rootNode->getNode("volume", true));
  _atis = PropertyObject<string>(_rootNode->getNode("atis", true));
  _addNoise = PropertyObject<bool>(_rootNode->getNode("add-noise", true));
  _cutoffSignalQuality = PropertyObject<double>(_rootNode->getNode("cutoff-signal-quality", true));

  _metarBridge->setAtisNode(_atis.node());
  _atis.node()->addChangeListener(&_atisSpeaker);

  // link the metar node. /environment/metar[3] is comm1 and /environment[4] is comm2.
  // see FGDATA/Environment/environment.xml
  _metarBridge->setMetarPropertiesRoot(fgGetNode("/environment", true)->getNode("metar", number() + 3, true));
  _metarBridge->bind();

  if (_useEightPointThree) {
    _useFrequencyFormatter = new EightPointThreeFrequencyFormatter(
                                                                   _rootNode->getNode("frequencies", true),
                                                                   "selected-mhz",
                                                                   "selected-mhz-fmt",
                                                                   "selected-channel-width-khz",
                                                                   "selected-real-frequency-mhz",
                                                                   "selected-channel"
                                                                   );
    _stbyFrequencyFormatter = new EightPointThreeFrequencyFormatter(
                                                                    _rootNode->getNode("frequencies", true),
                                                                    "standby-mhz",
                                                                    "standby-mhz-fmt",
                                                                    "standby-channel-width-khz",
                                                                    "standby-real-frequency-mhz",
                                                                    "standby-channel"
                                                                    );
  } else {
    _useFrequencyFormatter = new FrequencyFormatter(
                                                    _rootNode->getNode("frequencies/selected-mhz", true),
                                                    _rootNode->getNode("frequencies/selected-mhz-fmt", true),
                                                    0.025, 118.0, 137.0);

    _stbyFrequencyFormatter = new FrequencyFormatter(
                                                     _rootNode->getNode("frequencies/standby-mhz", true),
                                                     _rootNode->getNode("frequencies/standby-mhz-fmt", true),
                                                     0.025, 118.0, 137.0);


  }
}

void CommRadioImpl::unbind()
{
  _atis.node()->removeChangeListener(&_atisSpeaker);
  
  stopAudio();
  
  _metarBridge->unbind();
  AbstractInstrument::unbind();
}

void CommRadioImpl::init()
{
  initServicePowerProperties(_rootNode);

  string s;
  // initialize squelch to a sane value if unset
  s = _cutoffSignalQuality.node()->getStringValue();
  if (s.empty()) _cutoffSignalQuality = 0.4;

  // initialize add-noize to true if unset
  s = _addNoise.node()->getStringValue();
  if (s.empty()) _addNoise = true;
  
  auto soundManager = globals->get_subsystem<SGSoundMgr>();
  if (soundManager) {
    _sampleGroup = soundManager->find("atc", true);
  }
}

void CommRadioImpl::update(double dt)
{
  if (dt < SGLimitsd::min()) return;
  _stationTTL -= dt;

  // Ensure all output properties get written on exit of this method
  OnExit onExit(this);

  SGGeod position;
  try {
    position = globals->get_aircraft_position();
  }
  catch (std::exception &) {
    return;
  }

  if (!isServiceableAndPowered()) {
    _metarBridge->clearMetar();
    _atis = "";
    _stationTTL = 0.0;
    stopAudio();
    return;
  }

  if (_frequency != _useFrequencyFormatter->getFrequency()) {
    _frequency = _useFrequencyFormatter->getFrequency();
    _stationTTL = 0.0;
  }

  if (_stationTTL <= 0.0) {
    _stationTTL = 30.0;

    int freqKhz = static_cast<int>(_frequency * 1000 + 0.5);
    // make sure the frequency is integral multiple of 25, if not using 8.333 kHz spacing
    if (!_useEightPointThree && freqKhz % 25) {
        freqKhz += 5;
    }

    _commStationForFrequency = flightgear::CommStation::findByFreq(freqKhz, position, NULL);
  }

  if (false == _commStationForFrequency.valid()) {
    stopAudio();
    return;
  }
  
  _slantDistance_m = dist(_commStationForFrequency->cart(), SGVec3d::fromGeod(position));

  SGGeodesy::inverse(position, _commStationForFrequency->geod(), _trueBearingTo_deg, _trueBearingFrom_deg, _trackDistance_m);

  _heightAboveStation_ft = SGMiscd::max(0.0, position.getElevationFt() - _commStationForFrequency->airport()->elevation());

  _signalQuality_norm = _signalQualityComputer->computeSignalQuality(_slantDistance_m * SG_METER_TO_NM);
  _stationType = _commStationForFrequency->nameForType(_commStationForFrequency->type());
  _stationName = _commStationForFrequency->ident();
  _airportId = _commStationForFrequency->airport()->getId();

  _atisSpeaker.setStationId(_airportId);

  switch (_commStationForFrequency->type()) {
    case FGPositioned::FREQ_ATIS:
      case FGPositioned::FREQ_AWOS: {
      if (_signalQuality_norm > 0.01) {
        _metarBridge->requestMetarForId(_airportId);
      } else {
        _metarBridge->clearMetar();
        _atis = "";
      }
    }
      break;

    default:
      _metarBridge->clearMetar();
      _atis = "";
      break;
  }
  
  updateAudio();
}

void CommRadioImpl::updateAudio()
{
  if (!_sampleGroup)
    return;
  
  const string noiseRef = _soundPrefix + "_noise";
  const string atisRef = _soundPrefix + "_atis";

  SGSoundSample* noiseSample = _sampleGroup->find(noiseRef);
  
  // create noise sample if necessary, and play forever
  if (_addNoise && !noiseSample) {
    SGSharedPtr<SGSoundSample> noise = new SGSoundSample("Sounds/radionoise.wav", globals->get_fg_root());
    _sampleGroup->add(noise, noiseRef);
    _sampleGroup->play_looped(noiseRef);
    noiseSample = noise;
  }
  
  if (_atisSpeaker.hasSpokenAtis()) {
    // the speaker has created a new atis sample
    // remove previous atis sample
    _sampleGroup->remove(atisRef);
    
    SGSharedPtr<SGSoundSample> sample = _atisSpeaker.getSpokenAtis();
    _sampleGroup->add(sample, atisRef);
    _sampleGroup->play_looped(atisRef);
  }
  
  // adjust volumes
  const bool doSquelch = (_signalQuality_norm < _cutoffSignalQuality);
  double atisVolume = doSquelch ? 0.0 : _volume_norm;
  if (_addNoise) {
    const double noiseVol = (1.0 - _signalQuality_norm) * _volume_norm;
    atisVolume = _signalQuality_norm * _volume_norm;
    noiseSample->set_volume(doSquelch ? 0.0: noiseVol);
  }
  
  SGSoundSample* s = _sampleGroup->find(atisRef);
  if (s) {
    s->set_volume(atisVolume);
  }
}
  
void CommRadioImpl::stopAudio()
{
  if (_sampleGroup) {
    _sampleGroup->remove(_soundPrefix + "_noise");
    _sampleGroup->remove(_soundPrefix + "_atis");
  }
}

SGSubsystem * CommRadio::createInstance(SGPropertyNode_ptr rootNode)
{
  return new CommRadioImpl(rootNode);
}


// Register the subsystem.
#if 0
SGSubsystemMgr::InstancedRegistrant<CommRadio> registrantCommRadio(
    SGSubsystemMgr::FDM,
    {{"instrumentation", SGSubsystemMgr::Dependency::HARD}});
#endif

} // namespace Instrumentation

