#!perl

# Compute canon voices via the Music::Canon module.

use strict;
use warnings;

use File::Basename qw/basename/;
use Getopt::Long qw/GetOptionsFromArray/;

use Music::Canon        ();
use Music::LilyPondUtil ();

my $PROG_NAME = basename($0);

my %Modes = (
  exact => \&do_exact,
  modal => \&do_modal,
);

my $Flag_Non_Octave_Scale = 0;
my @Std_Opts              = (
  'contrary!'   => \my $Flag_Contrary,
  'raw!'        => \my $Flag_Raw,
  'relative=s'  => \my $Flag_Relative,
  'retrograde!' => \my $Flag_Retrograde,
  'transpose=s' => \my $Flag_Transpose,
);

# NOTE may false positive if there is a standalone argument option with
# the same contents as a mode name; however, with ZSH tab completion I'm
# using --foo=bar instead of the risk prone --foo bar so meh.
my @leading_args;
while ( @ARGV and !exists $Modes{ $ARGV[0] } ) {
  push @leading_args, shift @ARGV;
}

GetOptionsFromArray( \@leading_args, @Std_Opts, 'help|h' => \&emit_help, )
  or emit_help();

my $mode = shift;
emit_help() if !defined $mode or !exists $Modes{$mode};

if ( !@ARGV or @ARGV == 1 and $ARGV[0] eq '-' ) {
  chomp( @ARGV = <STDIN> );
}

my $ret;
eval { $ret = $Modes{$mode}->(@ARGV) };
if ($@) {
  chomp $@;
  warn "$PROG_NAME: $@\n";
  $ret = 65;    # assume input data error
}
exit $ret;

########################################################################
#
# SUBROUTINES

# Create these late from each mode as options to them might arrive
# globally or as mode options.
sub _init_obj {
  my $c = Music::Canon->new(
    contrary          => $Flag_Contrary,
    non_octave_scales => $Flag_Non_Octave_Scale,
    retrograde        => $Flag_Retrograde,
    transpose         => defined $Flag_Transpose ? $Flag_Transpose : 0,
  );
  my $l = Music::LilyPondUtil->new(
    max_pitch  => 999,
    min_pitch  => -999,
    mode       => 'relative',
    keep_state => 1,
  );
  $l->mode( defined $Flag_Relative ? 'relative' : 'absolute' );
  $l->prev_note($Flag_Relative) if defined $Flag_Relative;

  return $c, $l;
}

sub _parse_modal_opt {
  my ($mode_str) = @_;
  return 'major' unless defined $mode_str;

  my ( $asc, $dsc ) = split ':', $mode_str, 2;
  my @asc = split ',', $asc;

  my @dsc;
  if ( defined $dsc ) {
    @dsc = split ',', $dsc;
  }

  my @ret = grep defined, @asc > 1 ? \@asc : $asc[0],
    @dsc > 1 ? \@dsc : $dsc[0];
  return @ret;
}

sub emit_help {
  warn <<"EOH";
Usage: $PROG_NAME [global opts] mode [mode opts] args

Compute canon voices via the Music::Canon module. The modes are:

  exact - exact interval canon for given input notes
  modal - modal interval canon          "

Raw pitch numbers or lilypond note names are accepted. Examples:

  \$ $PROG_NAME --contrary --retrograde exact c cis d
  \$ $PROG_NAME --transpose=12 exact c e g
  \$ $PROG_NAME --raw --contrary exact 1 2 3

  \$ $PROG_NAME modal --input=lydian --output=locrian c e g

Run perldoc(1) on $PROG_NAME for additional documentation.

EOH
  exit 64;
}

sub emit_help_exact {
  warn <<"EOH";
Usage: $PROG_NAME [opts] exact [exactopts] note1 note2 .. noteN

Exact interval mapping. Options include:

  --raw           Emit raw pitch numbers in output instead of note names.
  --relative=N    Input is relative to lilypond note N (absolute notation is
                  assumed by default otherwise).
  --contrary      Compute canon in contrary motion.
  --retrograde    Reverse the output voice.
  --transpose=N   Transpose output voice by semitones or to a lilypond note.

Run perldoc(1) on $PROG_NAME for additional documentation.

EOH
  exit 64;
}

sub emit_help_modal {
  warn <<"EOH";
Usage: $PROG_NAME [opts] modal --input=mode --output=mode notes...

Modal interval mapping. In particular:

  --input=mode   Set the input and output modes. These may be Music::Scales
  --output=mode  names, or Forte Numbers. Major to Major used by default.

  --nos          Allow for non-octave scales. Necessary if doing crazy
                 things with intervals in the --input or --output options.

  --undef=N      Use N for notes that cannot be converted.

General options include:

  --raw          Emit raw pitch numbers in output instead of note names.
  --relative=N   Input is relative to lilypond note N (absolute notation is
                 assumed by default otherwise).
  --contrary     Compute canon in contrary motion.
  --retrograde   Reverse the output voice.
  --transpose=N  Transpose output voice by semitones or to a lilypond note.

Run perldoc(1) on $PROG_NAME for additional documentation.

EOH
  exit 64;
}

sub do_exact {
  my @args = @_;
  GetOptionsFromArray( \@args, @Std_Opts, 'help|h' => \&emit_help_exact, )
    or emit_help_exact();
  my ( $canon, $lyu ) = _init_obj();

  my @pitches = $lyu->notes2pitches( map split, @args );
  my @new_phrase = $canon->exact_map( \@pitches );

  print join( ' ', $Flag_Raw ? @new_phrase : $lyu->p2ly(@new_phrase) ), "\n";
  return 0;
}

sub do_modal {
  my @args = @_;
  GetOptionsFromArray(
    \@args, @Std_Opts,
    'help|h'   => \&emit_help_modal,
    'input=s'  => \my $input_mode,
    'nos!'     => \$Flag_Non_Octave_Scale,
    'output=s' => \my $output_mode,
    'undef=s'  => \my $whoops_undef,
  ) or emit_help_modal();
  my ( $canon, $lyu ) = _init_obj();

  $canon->set_scale_intervals( 'input', _parse_modal_opt($input_mode) )
    if defined $input_mode;
  $canon->set_scale_intervals( 'output', _parse_modal_opt($output_mode) )
    if defined $output_mode;

  $whoops_undef //= 'x';

  my @pitches = $lyu->notes2pitches( map split, @args );
  my @new_phrase;
  for my $p (@pitches) {
    my $np;
    eval { $np = $canon->modal_map($p); };
    if ($@) {
      if ( $@ =~ m/^undefined chromatic conversion/ ) {
        $np = $whoops_undef;
      } else {
        chomp $@;
        die "$@\n";
      }
    }
    push @new_phrase, $np;
  }
  @new_phrase = reverse @new_phrase if $canon->get_retrograde;

  print join( ' ', $Flag_Raw ? @new_phrase : $lyu->p2ly(@new_phrase) ), "\n";
  return 0;
}

__END__

=head1 NAME

canonical - compute canon voices via the Music::Canon module

=head1 SYNOPSIS

  $ canonical --contrary --retrograde exact c cis d
  $ canonical --transpose=12 exact c e g
  $ canonical --raw --contrary exact 1 2 3

  $ canonical modal --input=lydian --output=locrian c e g

=head1 DESCRIPTION

Command line interface to mapping methods present in the L<Music::Canon>
module. Custom questions might be better answered by coding directly
against L<Music::Canon>, see the C<eg/> directory under the distribution
of that Perl module for examples.

=head1 GLOBAL OPTIONS

This script currently supports the following global command line
switches. These can also be specified to each of the underlying modes.

=over 4

=item B<--contrary> B<--nocontrary>

Whether to compute output line in contrary motion or not.

=item B<--help>

Emit some help information and exit.

=item B<--raw> B<--noraw>

Whether to emit output in raw pitch numbers or lilypond note names.

=item B<--relative>=I<lilypond_note>

Use relative mode in L<Music::LilyPondUtil> and make the input notes
relative to the specified note. Without this option, the assumption is
that the lilypond input is specified in absolute form:

  g d\'                 # MIDI pitches 55 62 (absolute)
  --relative=g\' g d\'  # MIDI pitches 67 74 (relative to g' or 67)

=item B<--retrograde> B<--noretrograde>

Whether to reverse the output phrase or not.

=item B<--transpose>=I<pitch_or_lilypond_note>

Transpose to the first note of the output phrase by the specified amount
in semitones (integer) or to the specified lilypond note name.

=back

=head1 MODES

=head2 EXACT

Exact interval canon computation. No new options beyond the global ones
listed above.

  $ canonical exact --transpose=e c e g

=head2 MODAL

Modal interval canon computation. In addition to the global options
listed above, accepts:

=over 4

=item B<--input>=I<scale_or_forte_number>

Scale name (see L<Music::Scales>) or Forte Number to use for the input.
A colon delimits the ascending versus descending data; commas delimit
specific scale degrees. Examples:

  --input=mm
  --input=major:minor
  --input=2,2,2,2,1:5-25

See L<Music::Canon> for the algorithm that maps input to output mode.

=item B<--nos>

Allow non-octave scales. Necessary if the scale intervals sum up to
more than 12, or if scales must repeat before the usual 12-pitch
octave point.

=item B<--output>=I<scale_or_forte_number>

Like B<--input>, except for the output line.

=item B<--undef>=I<string>

String to use for notes or pitches that cannot be converted. If unset,
defaults to C<x>. For example, under contrary motion, using Major to
Major scales, C to D via C sharp is impossible, as there is no space
between C and B downwards in the output line for the chromatic C sharp:

  $ canonical --relative=c modal --contrary --undef=OOPS c cis d
  c OOPS b

=back

=head1 FILES

A ZSH completion script is available in the C<zsh-compdef/> directory of 
the L<App::MusicTools> distribution. Install this to a C<$fpath> 
directory.

=head1 BUGS

=head2 Reporting Bugs

If the bug is in the latest version, send a report to the author.
Patches that fix problems or add new features are welcome.

L<http://github.com/thrig/App-MusicTools>

=head1 SEE ALSO

L<http://www.lilypond.org/> and in particular the Learning and Notation
manuals should be consulted to understand lilypond note syntax. Or, use
raw pitch numbers.

L<http://en.wikipedia.org/wiki/Forte_number>

L<Music::Canon>, L<Music::LilyPondUtil>, L<Music::Scales>

=head1 AUTHOR

Jeremy Mates

=head1 COPYRIGHT

Copyright (C) 2013 by Jeremy Mates

This script is free software; you can redistribute it and/or modify it
under the same terms as Perl itself, either Perl version 5.16 or, at
your option, any later version of Perl 5 you may have available.

=cut
