package App::SourcePlot::Source;

=head1 NAME

App::SourcePlot::Source - creates a observation source

=head1 SYNOPSIS

 use App::SourcePlot::Source;
 $src = new App::SourcePlot::Source;

=head1 DESCRIPTION

This class will create Source objects that will hold essential
information for any single source.

It is essentially a wrapper around an Astro::Coords object to add
the additional information used to display a source in this
application.

=cut

use 5.004;
use Carp;
use strict;

use Astro::Coords;
use Math::Trig qw/pi/;
use DateTime;
use DateTime::Format::Strptime;

our $VERSION = '1.30';

my $locateBug = 0;

=head1 PUBLIC METHODS

These are the methods avaliable in this class:

=over 4

=item new

Create a new Source object.

  $obs = new App::SourcePlot::Source($planet);
  $obs = new App::SourcePlot::Source($name, $RA, $DEC, $Epoc);

Or using an Astro::Coords object.

  $coords = new Astro::Coords(...);
  $obs = new App::SourcePlot::Source($coords);

=cut

sub new {
  print "Creating a new observation Source object\n" if $locateBug;

  my $proto = shift;
  my $class = ref($proto) || $proto;

  my $self = {};  # Anon hash

  bless($self, $class);
  print "New observation Source object has been blessed: $self\n" if $locateBug;

  $self->configure(@_);

  $self->active(1);

  print "Object created\n" if $locateBug;

  return $self;
}


sub configure {
  my $self = shift;

  # Special case: empty source object.
  unless (@_) {
    $self->coords(new Astro::Coords());
    return;
  }

  my $name = shift;

  if (UNIVERSAL::isa($name, 'Astro::Coords')) {
    $self->coords($name);
  }
  elsif (@_) {
    print "Passed in paramaters are being entered\n" if $locateBug;
    my ($ra, $dec, $epoc, undef) = @_;

    # Prevent Astro::Coords guessing between radians and degrees.
    my $unit = ($ra =~ /:/ or $dec =~ /:/)
             ? 'sexagesimal'
             : 'degrees';

    if ($epoc eq 'RJ') {
      $self->coords(new Astro::Coords(
        name => $name,
        ra => ($unit eq 'degrees') ? ($ra * 15) : $ra,
        dec => $dec,
        type => 'J2000',
        units => $unit,
      ));
    }
    elsif ($epoc eq 'RB') {
      $self->coords(new Astro::Coords(
        name => $name,
        ra => ($unit eq 'degrees') ? ($ra * 15) : $ra,
        dec => $dec,
        type => 'B1950',
        units => $unit,
      ));
    }
    elsif ($epoc eq 'GA') {
      $self->coords(new Astro::Coords(
        name => $name,
        long => $ra,
        lat => $dec,
        type => 'galactic',
        units => $unit,
      ));
    }
    elsif ($epoc eq 'AZ') {
      $self->coords(new Astro::Coords(
        name => $name,
        az => $ra,
        el => $dec,
        units => $unit,
      ));
    }
    else {
      die "App::SourcePlot::Source unknown epoc " . $epoc .
           " for source " . $name;
    }
  }
  else {
    $self->coords(new Astro::Coords(
      planet => $name
    ));
  }
}

############################################################
#  Common data manipulation functions
#
=item name

returns and sets the name of the source

  $name = $obs->name();
  $obs->name('Mars');

=cut

sub name {
  my $self = shift;
  return $self->coords()->name(@_);
}

=item coords

Set or return the corresponding Astro::Coords object.

=cut

sub coords {
  my ($self, $coords, undef) = @_;
  if (defined $coords) {
    die unless UNIVERSAL::isa($coords, 'Astro::Coords');
    $self->{'COORDS'} = $coords;
  }
  return $self->{'COORDS'};
}

=item active

returns and sets whether the source is active

  $on = $obs->active();
  $obs->active(0);

=cut

sub active {
  my $self = shift;
  $self->{ACTIVE} = shift if @_;
  return $self->{ACTIVE} if defined $self->{ACTIVE};
  return '';
}

=item color

returns and sets the source color

  $col = $obs->color();
  $obs->color('black');

=cut

sub color {
  my $self = shift;
  $self->{COLOR} = shift if @_;
  return $self->{COLOR} if defined $self->{COLOR};
  return '';
}

=item lineWidth

returns and sets the sources thickness

  $LW = $obs->lineWidth();
  $obs->lineWidth(2);

=cut

sub lineWidth {
  my $self = shift;
  $self->{LINEWIDTH} = shift if @_;
  return $self->{LINEWIDTH} if defined $self->{LINEWIDTH};
  return 1;
}

=item index

returns and sets the sources window index

  $index = $obs->index();
  $obs->index(1234);

=cut

sub index {
  my $self = shift;
  $self->{INDEX} = shift if @_;
  return $self->{INDEX} if defined $self->{INDEX};
  return -1;
}

=item ra

Returns the RA of the source, or other coordinate type
in systems other than RJ / RB.

  $ra = $obs->ra();

=cut

sub ra {
  my $self = shift;
  if (@_) {
    die 'App::SourcePlot::Source cannot change ra';
  }
  my $native_method = $self->coords()->native();
  my ($ra, undef) = $self->coords()->$native_method();
  return sprintf('%.4f', $ra->degrees()) if $self->epoc() eq 'GA';
  return $ra->in_format('sexagesimal');
}

=item dec

Returns the declination of the source, or other coordinate
type in systems other than RJ / RB.

  $dec = $obs->dec();

=cut

sub dec {
  my $self = shift;
  if (@_) {
    die 'App::SourcePlot::Source cannot change dec';
  }
  my $native_method = $self->coords()->native();
  my (undef, $dec) = $self->coords()->$native_method();
  return sprintf('% .4f', $dec->degrees()) if $self->epoc() eq 'GA';
  return $dec->in_format('sexagesimal');
}

=item ra2000

returns the ra of the source in J2000 in radians

  $ra2000 = $obs->ra2000();

=cut

sub ra2000 {
  my $self = shift;
  if (@_) {
    die 'App::SourcePlot::Source cannot change ra2000';
  }
  return $self->coords()->ra(format => 'r');
}

=item dec2000

returns dec of the source in J2000 in radians

  $dec2000 = $obs->dec2000();

=cut

sub dec2000 {
  my $self = shift;
  if (@_) {
    die 'App::SourcePlot::Source cannot change dec';
  }
  return $self->coords()->dec(format => 'r');
}

=item epoc

returns the epoch of the source

  $epoc = $obs->epoc();

=cut

sub epoc {
  my $self = shift;
  my $native_method = $self->coords()->native;

  return 'RJ' if $native_method eq 'radec';
  return 'RB' if $native_method eq 'radec1950';
  return 'GA' if $native_method eq 'glonglat';
  return 'AZ' if $native_method eq 'azel';
  return '??';
}

=item elevation

returns the current elevation of the source at the ut time
in degrees

  $ele = $obs->elevation();

=cut

sub elevation {
  my $self = shift;
  if (@_) {
    die 'App::SourcePlot::Source cannot set elevation';
  }
  return $self->coords()->el(format => 'd');
}

=item is_blank

Returns true if the source information is "blank".  This is the
default state for an object constructed with no arguments,
and is represented by the Astro::Coords default type -- a
Calibration object.

=cut

sub is_blank {
    my $self = shift;

    return $self->coords()->type() eq 'CAL';
}

=item NameX

returns and sets the current x position of name label

  $x = $obs->NameX();
  $obs->NameX(6.5);

=cut

sub NameX {
  my $self = shift;
  $self->{NAMEX} = shift if @_;
  return $self->{NAMEX} if defined $self->{NAMEX};
  return '';
}

=item NameY

returns and sets the current y position of name label

  $y = $obs->NameY();
  $obs->NameY(6.5);

=cut

sub NameY {
  my $self = shift;
  $self->{NAMEY} = shift if @_;
  return $self->{NAMEY} if defined $self->{NAMEY};
  return '';
}

=item AzElOffsets

returns the amount in the current system to offset to draw the
Elevation and Azimuth axes

  ($elex, $eley, $azx, $azy) = $obs->AzElOffsets();
  $obs->AzElOffsets(.5, 4, .3, 2);

=cut

sub AzElOffsets {
  my $self = shift;
  if (@_) {
    $self->{ELEX} = shift;
    $self->{ELEY} = shift;
    $self->{AZX} = shift;
    $self->{AZY} = shift;
  }
  return ($self->{ELEX}, $self->{ELEY}, $self->{AZX}, $self->{AZY}) if defined $self->{ELEX};
  return (undef, undef, undef, undef);
}

=item timeDotX

returns and sets the current position of the time dot on
the x axis

  $x = $obs->timeDotX();
  $obs->timeDotX('15.122');

=cut

sub timeDotX {
  my $self = shift;
  $self->{TIMEDOTX} = shift if @_;
  return $self->{TIMEDOTX} if defined $self->{TIMEDOTX};
  return '';
}

=item timeDotY

returns and sets the current position of the time dot on
the y axis

  $y = $obs->timeDotY();
  $obs->timeDotY('15.122');

=cut

sub timeDotY {
  my $self = shift;
  $self->{TIMEDOTY} = shift if @_;
  return $self->{TIMEDOTY} if defined $self->{TIMEDOTY};
  return '';
}

=item time_ele_points

These functions return an array of comparative points for different
characteristics of this source.  The avaliable comparisons are:

  time_ele_points       - time vs elevation
  time_az_points       - time vs azimuth
  time_pa_points       - time vs parallactic angle
  ele_time_points       - elevation vs time
  ele_az_points       - elevation vs azimuth
  ele_pa_points       - elevation vs parallactic angle
  az_time_points       - azimuth vs time
  az_ele_points       - azimuth vs azimuth
  az_pa_points       - azimuth vs parallactic angle
  pa_time_points       - parallactic angle vs time
  pa_ele_points       - parallactic angle vs elevation
  pa_az_points       - parallactic angle vs azimuth

  Example syntax:

  @time_ele_points = $obs->time_ele_points();

=cut

sub time_ele_points {
  my $self = shift;
  return @{$self->{TIME_ELE_POINTS}} if defined $self->{TIME_ELE_POINTS};
  return ();
}

sub time_az_points {
  my $self = shift;
  return @{$self->{TIME_AZ_POINTS}} if defined $self->{TIME_AZ_POINTS};
  return ();
}

sub time_pa_points {
  my $self = shift;
  return @{$self->{TIME_PA_POINTS}} if defined $self->{TIME_PA_POINTS};
  return ();
}

sub ele_time_points {
  my $self = shift;
  return @{$self->{ELE_TIME_POINTS}} if defined $self->{ELE_TIME_POINTS};
  return ();
}

sub ele_az_points {
  my $self = shift;
  return @{$self->{ELE_AZ_POINTS}} if defined $self->{ELE_AZ_POINTS};
  return ();
}

sub ele_pa_points {
  my $self = shift;
  return @{$self->{ELE_PA_POINTS}} if defined $self->{ELE_PA_POINTS};
  return ();
}

sub az_time_points {
  my $self = shift;
  return @{$self->{AZ_TIME_POINTS}} if defined $self->{AZ_TIME_POINTS};
  return ();
}

sub az_ele_points {
  my $self = shift;
  return @{$self->{AZ_ELE_POINTS}} if defined $self->{AZ_ELE_POINTS};
  return ();
}

sub az_pa_points {
  my $self = shift;
  return @{$self->{AZ_PA_POINTS}} if defined $self->{AZ_PA_POINTS};
  return ();
}

sub pa_time_points {
  my $self = shift;
  return @{$self->{PA_TIME_POINTS}} if defined $self->{PA_TIME_POINTS};
  return ();
}

sub pa_ele_points {
  my $self = shift;
  return @{$self->{PA_ELE_POINTS}} if defined $self->{PA_ELE_POINTS};
  return ();
}

sub pa_az_points {
  my $self = shift;
  return @{$self->{PA_AZ_POINTS}} if defined $self->{PA_AZ_POINTS};
  return ();
}

############################################################
#  Some needed methods - not calculations but info gluers
#
=item dispLine

returns the line to display - presentation use

  $line = $obs->dispLine();

=cut

sub dispLine {
  my $self = shift;
  my $line;
  unless (UNIVERSAL::isa($self->coords(), 'Astro::Coords::Planet')) {
    $line = sprintf(' %-4d  %-16s  %-12s  %-13s  %-4s',
                   ($self->index() + 1), $self->name(),
                   $self->ra(), $self->dec(), $self->epoc());
  }
  else {
    $line = sprintf(' %-4d  %-16s  Planet',
                    ($self->index() + 1),
                    ucfirst($self->name()));
  }
  return $line;
}

=item copy

returns a copy of this object

  $cp = $obs->copy();

=cut

sub copy {
  my $self = shift;
  my $source = $self->new($self->coords());
  return $source;
}

=item calcPoints

Calculations the Elevation, Azimeth, etc. points
$MW is the main window widget.  Required for
progress bar

  $obs->calcPoints($date, $time, $num_points, $MW, $tel);

=cut

sub calcPoints {
  my $self = shift;
  my $DATE = shift;
  my $TIME = shift;
  my $numPoints = shift;
  my $MW = shift;
  my $tel = shift;
  my $timeBug = 0;

  my $coords = $self->coords();
  $coords->telescope($tel);
  my $dt_save = $coords->datetime();

  my $strp = new DateTime::Format::Strptime(
                 pattern => '%Y/%m/%d %H:%M:%S',
                 time_zone => 'UTC',
                 on_error => 'croak');

  my $dt = $strp->parse_datetime($DATE . ' ' .  $TIME);

  my $dt_running = $dt->clone();

  my $tlen = @{$self->{TIME_ELE_POINTS}} if defined $self->{TIME_ELE_POINTS};
  if (defined $tlen && $tlen > 0) {
    return;
  }

  $dt_running->subtract(hours => 2);
  my $lst_prev = undef;

  for (my $h = 0; $h < $numPoints; $h ++) {
    $MW->update;
    my ($lst, $ele, $az, $pa, undef) = $self->_calcPoint($dt_running);

    if (defined $lst_prev and $lst < $lst_prev) {
      $lst += 2 * pi;

      # Allow a second wrap around in case LST is just under 2 pi at the
      # start (eg on March 5th at JCMT with (default) 1:30:00 center time.
      # This is necessary because we generate points over a full day,
      # and then convert to LST so there is always one wrap-around, with
      # a potential for a second for certain date / location /center time
      # configurations!
      if ($lst < $lst_prev) {
          $lst += 2 * pi;
      }
    }
    $lst_prev = $lst;

    push (@{$self->{TIME_ELE_POINTS}}, $lst);
    push (@{$self->{TIME_ELE_POINTS}}, $ele);

    push (@{$self->{TIME_AZ_POINTS}}, $lst);
    push (@{$self->{TIME_AZ_POINTS}}, $az);

    push (@{$self->{TIME_PA_POINTS}}, $lst);
    push (@{$self->{TIME_PA_POINTS}}, $pa);

    push (@{$self->{ELE_TIME_POINTS}}, $ele);
    push (@{$self->{ELE_TIME_POINTS}}, $lst);

    push (@{$self->{ELE_AZ_POINTS}}, $ele);
    push (@{$self->{ELE_AZ_POINTS}}, $az);

    push (@{$self->{ELE_PA_POINTS}}, $ele);
    push (@{$self->{ELE_PA_POINTS}}, $pa);

    push (@{$self->{AZ_TIME_POINTS}}, $az);
    push (@{$self->{AZ_TIME_POINTS}}, $lst);

    push (@{$self->{AZ_ELE_POINTS}}, $az);
    push (@{$self->{AZ_ELE_POINTS}}, $ele);

    push (@{$self->{AZ_PA_POINTS}}, $az);
    push (@{$self->{AZ_PA_POINTS}}, $pa);

    push (@{$self->{PA_TIME_POINTS}}, $pa);
    push (@{$self->{PA_TIME_POINTS}}, $lst);

    push (@{$self->{PA_ELE_POINTS}}, $pa);
    push (@{$self->{PA_ELE_POINTS}}, $ele);

    push (@{$self->{PA_AZ_POINTS}}, $pa);
    push (@{$self->{PA_AZ_POINTS}}, $az);

    $dt_running->add(seconds => 24 * 3600 / ($numPoints - 1));
  }

  $coords->datetime($dt_save);
}

=item calcPoint

Returns the time in decimal, elevation, azimuth, and parallactic angle
for a given source at a particular time and date.

  ($lst, $ele, $az, $pa) = $obs->calcPoint($date, $time, $tel);

=cut

sub calcPoint {
  my $self = shift;
  my $DATE = shift;
  my $TIME = shift;
  my $tel = shift;

  my $strp = new DateTime::Format::Strptime(
                 pattern => '%Y/%m/%d %H:%M:%S',
                 time_zone => 'UTC',
                 on_error => 'croak');

  my $dt = $strp->parse_datetime($DATE . ' ' .  $TIME);

  $dt->add(hours => 10);

  return $self->_calcPoint($dt, $tel);
}

sub _calcPoint {
  my $self = shift;
  my $dt = shift;
  my $tel = shift;

  # PAL (and so Astro::Coords) can not handle seconds > 59 (used in the case
  # of leap seconds), so replace with 59 seconds when this happens.
  $dt->set_second(59) if $dt->second() > 59;

  my $coords = $self->coords();
  $coords->datetime($dt) if defined $dt;
  $coords->telescope($tel) if defined $tel;

  my $pa = $coords->pa(format => 'r');
  my ($elex, $eley) = _axis_direction($pa, 0, 30);
  my ($azx, $azy) = _axis_direction($pa, 30, 0);

  return ($coords->_lst()->radians(),
          $coords->el(format => 'd'),
          $coords->az(format => 'd'),
          $coords->pa(format => 'd'),
          $elex, $eley, $azx, $azy);
}

# Based on the AzToRa function from the old
# Astro::Instrument::SCUBA::Array module
# by Casey Best (University of Victoria).
sub _axis_direction {
  my $pa = shift;
  my $daz = shift;
  my $del = shift;

  my $x = -$daz * cos($pa) + $del * sin($pa);
  my $y = $daz * sin($pa) + $del * cos($pa);
  return ($x, $y);
}


=item erasePoints

Erases all of the plotting points.  Needed when new coords put in.

  $obs->erasePoints();

=cut

sub erasePoints {
  my $self = shift;
  $self->{TIME_ELE_POINTS} = ();
  $self->{TIME_AZ_POINTS} = ();
  $self->{TIME_PA_POINTS} = ();
  $self->{ELE_TIME_POINTS} = ();
  $self->{ELE_AZ_POINTS} = ();
  $self->{ELE_PA_POINTS} = ();
  $self->{AZ_TIME_POINTS} = ();
  $self->{AZ_ELE_POINTS} = ();
  $self->{AZ_PA_POINTS} = ();
  $self->{PA_TIME_POINTS} = ();
  $self->{PA_ELE_POINTS} = ();
  $self->{PA_AZ_POINTS} = ();
  $self->{TIMEDOTX} = undef;
  $self->{TIMEDOTY} = undef;
}

=item eraseTimeDot

Erases the time dot coordinates

  $obs->eraseTimeDot();

=cut

sub eraseTimeDot {
  my $self = shift;
  $self->{TIMEDOTX} = undef;
  $self->{TIMEDOTY} = undef;
}

=back

=head1 AUTHOR

Casey Best

=head1 COPYRIGHT

Copyright (C) 2018 East Asian Observatory.
Copyright (C) 2012-2014 Science and Technology Facilities Council.
Copyright (C) 1998, 1999 Particle Physics and Astronomy Research
Council. All Rights Reserved.

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
along with this program. If not, see <http://www.gnu.org/licenses/>.

=cut

1;
