#!perl

# Command line interface to the atonal routines in the Music::AtonalUtil
# module (and otherwise a dumping ground for music related wrangling).
#
# Run perldoc(1) on this file for additional documentation.
#
# A ZSH completion script is available in the zsh-compdef/ directory of
# the App::MusicTools distribution.

# XXX improve emit_pitch_set (returns a string, so caller can then do
# what it will -- or have LilyPondUtil sub that knows how to format a
# pitch set? (among other possible code cleanups/simplifications)

use strict;
use warnings;

use File::Basename qw/basename/;
use Getopt::Long qw/GetOptionsFromArray/;
use List::Util qw/sum/;
use List::MoreUtils qw/all any/;
use Music::AtonalUtil   ();
use Music::LilyPondUtil ();
use Music::Scala        ();
use Music::Scales qw/get_scale_nums/;
use Music::Tempo qw/bpm_to_ms/;
use Music::Tension::Cope ();
# untested with --tension
#use Music::Tension::PlompLevelt ();
use Parse::Range qw/parse_range/;
use Scalar::Util qw/looks_like_number/;

my $FORTE_NUMBER_RE = qr/[3-9]-[zZ]?\d{1,2}/;

my $PROG_NAME = basename($0);

my %modes = (
  basic                  => \&basic,
  circular_permute       => \&circular_permute,
  combos                 => \&combos,
  complement             => \&complement,
  equivs                 => \&equivs,
  findall                => \&findall,
  findin                 => \&findin,
  fnums                  => \&fnums,
  forte2pcs              => \&forte2pcs,
  freq2pitch             => \&freq2pitch,
  interval_class_content => \&interval_class_content,
  intervals2pcs          => \&intervals2pcs,
  invariance_matrix      => \&invariance_matrix,
  invariants             => \&invariants,
  invert                 => \&invert,
  ly2pitch               => \&ly2pitch,
  multiply               => \&multiply,
  normal_form            => \&normal_form,
  notes2time             => \&notes2time,
  pcs2forte              => \&pcs2forte,
  pcs2intervals          => \&pcs2intervals,
  pitch2freq             => \&pitch2freq,
  pitch2intervalclass    => \&pitch2intervalclass,
  pitch2ly               => \&pitch2ly,
  prime_form             => \&prime_form,
  recipe                 => \&recipe,
  retrograde             => \&retrograde,
  rotate                 => \&rotate,
  set_complex            => \&set_complex,
  subsets                => \&subsets,
  tcis                   => \&tcis,
  tcs                    => \&tcs,
  tension                => \&tension,
  transpose              => \&transpose,
  transpose_invert       => \&transpose_invert,
  variances              => \&variances,
  whatscalesfit          => \&whatscalesfit,
  zrelation              => \&zrelation,
);

my ( $Flag_Flat, $Flag_Lyout, $Flag_Quiet, $Flag_Tension );
my $Flag_Record_Sep = ',';    # mostly for pitch sets e.g. 0,4,7
my @Std_Opts        = (
  'flats!'    => \$Flag_Flat,
  'ly'        => \$Flag_Lyout,
  'quiet!'    => \$Flag_Quiet,
  'rs=s'      => \$Flag_Record_Sep,
  'tension=s' => \$Flag_Tension,
);

# 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'           => \&print_help,
  'listmodes'      => sub { print "$_\n" for sort keys %modes; exit 0 },
  'scaledegrees=s' => \my $scale_degrees,
) or print_help();
$scale_degrees //= 12;
my $mode = shift;

print_help() if !defined $mode or !exists $modes{$mode};

my $Atu = Music::AtonalUtil->new( DEG_IN_SCALE => $scale_degrees );
my $Lyu = Music::LilyPondUtil->new(
  chrome => ( $Flag_Flat ? 'flats' : 'sharps' ),
  ignore_register => 1,
  keep_state      => 0,
  mode            => 'relative'
);
my $Tension;

$modes{$mode}->(@ARGV);
exit 0;

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

sub _ms2abbr_time {
  my $difference = int shift;

  return "0ms" if $difference == 0;

  my $ms = $difference % 1000;
  $difference = ( $difference - $ms ) / 1000;
  my $seconds = $difference % 60;
  $difference = ( $difference - $seconds ) / 60;
  my $minutes = $difference % 60;
  $difference = ( $difference - $minutes ) / 60;

  #  my $hours = $difference;
  my $hours = $difference % 24;
  $difference = ( $difference - $hours ) / 24;
  my $days  = $difference % 7;
  my $weeks = ( $difference - $days ) / 7;

  # better way to do this?
  my $temp = ($weeks) ? "${weeks}w " : q{};
  $temp .= ($days)    ? "${days}d "    : q{};
  $temp .= ($hours)   ? "${hours}h "   : q{};
  $temp .= ($minutes) ? "${minutes}m " : q{};
  $temp .= ($seconds) ? "${seconds}s " : q{};
  $temp .= ($ms)      ? "${ms}ms"      : q{};
  return $temp;
}

# utility for whatscalesfit
sub _fit_scale {
  my ( $scale, $pset, $scale_set, $is_dsc ) = @_;

  my $dsc_flag = $is_dsc ? 'DSC' : ' ';

  for my $transpose ( 0 .. 11 ) {
    my @scale_nums = map { my $t = $_ + $transpose; $t } @$scale_set;
    my %seen;
    @seen{ map { my $p = $_ % $scale_degrees; $p } @scale_nums } = ();
    if ( all { exists $seen{$_} } @$pset ) {
      my $tonic = $Lyu->p2ly($transpose);
      $tonic =~ s/is/#/;
      $tonic =~ s/es/b/;

      my $scale_fmt = join '', ('%-6s') x @scale_nums;
      my $scale_notes = sprintf $scale_fmt, $Lyu->p2ly(@scale_nums);

      printf "%-3s%-18s%-8s%s\n", "\u$tonic", "\u$scale", $dsc_flag,
        $scale_notes;
    }
  }
}

sub _init_scala {
  my ( $cf, $cp, $file ) = @_;
  $cf //= 440;    # A440
  $cp //= 69;

  my $scala = Music::Scala->new(
    concertfreq  => $cf,
    concertpitch => $cp,
  );
  if ( defined $file ) {
    $scala->read_scala($file);
    my @ratios = $scala->get_ratios;
    if ( @ratios != 12 or $ratios[-1] != 2 ) {
      # XXX until figure out .kbm file support?
      die "scala scales must be 12-tone and octave bounded\n";
    }
  }

  return $scala;
}

sub _init_tension {
  my ( $override, $ref_freq ) = @_;
  $Flag_Tension = $override if defined $override;

  if ( defined $Flag_Tension ) {
    if ( $Flag_Tension eq 'cope' ) {
      $Tension =
        Music::Tension::Cope->new(
        defined $ref_freq ? ( reference_frequency => $ref_freq ) : () );
      # untested with --tension
      #   } elsif ( $Flag_Tension eq 'pl' ) {
      #     $Tension = Music::Tension::PlompLevelt->new;
    } else {
      die "unknown tension method '$Flag_Tension'\n";
    }
  }
}

sub args2pitchset {
  my (@args) = @_;

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

  my $pitch_set;
  if ( $args[0] =~ m/($FORTE_NUMBER_RE)/ ) {
    $pitch_set = $Atu->forte2pcs($1);
    die "unknown Forte Number '$args[0]'\n" if !defined $pitch_set;
  } else {
    for my $arg (@args) {
      for my $p ( $arg =~ /([-\d\w]+)/g ) {
        push @$pitch_set, $Lyu->notes2pitches($p);
      }
    }
  }

  return $pitch_set;
}

sub basic {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
  $Lyu->chrome('flats') if $Flag_Flat;

  my $pset = args2pitchset(@args);

  emit_pitch_set( $Atu->prime_form($pset), rs => $Flag_Record_Sep );
  emit_pitch_set(
    scalar $Atu->interval_class_content($pset),
    lyflag => 0,
    rs     => '',
  );

  my $forte = $Atu->pcs2forte($pset) // '';
  print $forte, "\n";

  if ($Flag_Tension) {
    _init_tension();
    printf "%.03f  %.03f  %.03f\n", $Tension->vertical($pset);
  }
}

sub circular_permute {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
  $Lyu->chrome('flats') if $Flag_Flat;
  emit_pitch_set( $Atu->circular_permute( args2pitchset(@args) ) );
}

sub combos {
  my (@args) = @_;
  my $mode = 'absolute';
  GetOptionsFromArray(
    \@args, @Std_Opts,
    'concertfreq|cf=s'  => \my $concert_freq,
    'concertpitch|cp=s' => \my $concert_pitch,
    'pitches'           => \my $Flag_Pitches,
    'relative=s'        => \my $relative,
    'scala=s'           => \my $scala_file,
  ) or print_help();
  $Lyu->chrome('flats') if $Flag_Flat;

  $Lyu->ignore_register(0);
  $Lyu->keep_state(1);
  $Lyu->sticky_state(1);
  if ($relative) {
    $Lyu->mode('relative');
    $Lyu->prev_note($relative);
  } else {
    $Lyu->mode('absolute');
  }

  my $scala = _init_scala( $concert_freq, $concert_pitch, $scala_file );

  my @freqs;
  if ( !@args or ( @args == 1 and $args[0] eq '-' ) ) {
    while ( my $line = readline STDIN ) {
      push @freqs, split ' ', $line;
    }
  } else {
    for my $arg (@args) {
      push @freqs, split ' ', $arg;
    }
  }

  if ( @freqs < 2 ) {
    die "Usage: $0 combos [--pitches [--relative=note]] f1 f2 [f3...]\n";
  }

  # turn on pitch mode if first note looks more a note than a number
  if ( $Flag_Pitches or $freqs[0] =~ m/[a-g]/ ) {
    @freqs = map $scala->pitch2freq($_), $Lyu->notes2pitches(@freqs);
  }

  for my $i ( 1 .. $#freqs ) {
    my $plus  = $freqs[0] + $freqs[1];
    my $minus = $freqs[$i] - $freqs[0];

    # (try to) Figure out MIDI pitch of combination tone, and what the
    # error is due to presumed equal temperament tuning of said MIDI
    # pitches.
    my $plus_pitch  = 0;
    my $minus_pitch = 0;
    my $plus_delta  = 0;
    my $minus_delta = 0;
    my $errstr      = '';
    eval {
      # XXX if instead have $scala_file, then use $scala->get_concertpitch
      # hmm.. how do we know freq2pitch mapping? do not have appropriate code
      # for the scala input side of things - or get pitch using this method,
      # then search around in that space by interval to see what of the scala
      # intervals get you the closest, use that as the frequency to compare
      # with the combo tone? - but only that logic if have a scala_file defined
      $plus_pitch  = $scala->freq2pitch($plus);
      $minus_pitch = $scala->freq2pitch($minus);
      $plus_delta  = $scala->pitch2freq($plus_pitch) - $plus;
      $minus_delta = $scala->pitch2freq($minus_pitch) - $minus;
    };
    if ($@) {
      $errstr = "\t/!\\ pitch out of bounds";
    }

    # best effort to get a note name, revert to pitch numbers if out of range
    if ( $Flag_Lyout and length $errstr == 0 ) {
      eval {
        $plus_pitch  = $Lyu->p2ly($plus_pitch);
        $minus_pitch = $Lyu->p2ly($minus_pitch);
      };
      if ($@) {
        $errstr = "\t/!\\ ly note out of bounds";
      }
    }

    printf "%.2f+%.2f = %.2f\t(%s error %.2f)%s\n", $freqs[0], $freqs[$i],
      $plus,
      $plus_pitch, $plus_delta, $errstr;
    printf "%.2f-%.2f = %.2f\t(%s error %.2f)%s\n", $freqs[$i], $freqs[0],
      $minus,
      $minus_pitch, $minus_delta, $errstr;
  }
}

sub complement {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
  $Lyu->chrome('flats') if $Flag_Flat;
  emit_pitch_set( $Atu->complement( args2pitchset(@args) ) );
}

sub emit_pitch_set {
  my ( $pset, %params ) = @_;

  my $lyify = exists $params{lyflag} ? $params{lyflag} : $Flag_Lyout;
  my $rs    = exists $params{rs}     ? $params{rs}     : ' ';

  my $has_nl = 0;
  my $str    = '';
  for my $i (@$pset) {
    if ( ref $i eq 'ARRAY' ) {
      $has_nl = emit_pitch_set( $i, %params );
    } else {
      $str .= ( $lyify ? $Lyu->p2ly($i) : $i ) . $rs;
    }
  }
  $str =~ s/$rs\z//;
  $str .= "\n" unless $has_nl;
  print $str;
  return 1;
}

sub equivs {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
  $Lyu->chrome('flats') if $Flag_Flat;

  my ( @transpose, @transpose_invert, %seen );
  my $pset = args2pitchset(@args);

  for my $i ( 0 .. $Atu->scale_degrees - 1 ) {
    my $set = ( $Atu->normal_form( $Atu->transpose( $i, $pset ) ) )[0];
    if ( !$seen{"@$set"}++ ) {
      push @transpose, $set;
      my $iset =
        ( $Atu->normal_form( $Atu->transpose_invert( $i, 0, $pset ) ) )[0];
      push @transpose_invert, $iset if !$seen{"@$iset"}++;
    }
  }

  emit_pitch_set( \@transpose )        if @transpose;
  emit_pitch_set( \@transpose_invert ) if @transpose_invert;
}

sub findall {
  my (@args) = @_;
  GetOptionsFromArray(
    \@args, @Std_Opts,
    'exclude=s' => \my $excludes,
    'fn=s'      => \my $desired_forte_nums,
    'root=s'    => \my $root_pitch
  );
  $Lyu->chrome('flats') if $Flag_Flat;

  my $desired = args2pitchset(@args);
  my %excludes;
  @excludes{ $Lyu->notes2pitches( split /[, ]+/, $excludes ) } = ()
    if defined $excludes;
  $root_pitch = $Lyu->notes2pitches($root_pitch) if defined $root_pitch;
  my $fn_re = '^[' . join( '', parse_range($desired_forte_nums) ) . ']$';

  my $fnums = $Atu->fnums;
  for my $fnum ( sort keys %$fnums ) {
    if ( defined $desired_forte_nums ) {
      ( my $prefix = $fnum ) =~ s/[-].+//;
      next if $prefix !~ m/$fn_re/;
    }
    _findps( $fnums->{$fnum}, $desired, $root_pitch, \%excludes, $fnum );
  }
}

sub findin {
  my (@args) = @_;
  GetOptionsFromArray(
    \@args, @Std_Opts,
    'exclude=s'    => \my $excludes,
    'pitchset|P=s' => \my $base_input,
    'root=s'       => \my $root_pitch
  ) or print_help();
  $Lyu->chrome('flats') if $Flag_Flat;

  my $ps_base;
  if ( $base_input =~ m/^\d-/ ) {
    $ps_base = $Atu->forte2pcs($base_input);
    die "unknown Forte Number '$base_input'\n" if !defined $ps_base;
  } else {
    $ps_base = args2pitchset( split /[ ,]/, $base_input );
  }
  my $desired = args2pitchset(@args);
  my %excludes;
  @excludes{ $Lyu->notes2pitches( split /[, ]+/, $excludes ) } = ()
    if defined $excludes;
  $root_pitch = $Lyu->notes2pitches($root_pitch) if defined $root_pitch;

  if ( @$desired > @$ps_base ) {
    die "cannot desire more than is present\n";
  }

  _findps( $ps_base, $desired, $root_pitch, \%excludes );
}

sub _findps {
  my ( $ps_base, $desired, $root_pitch, $excludes, $fnum ) = @_;
  $fnum //= '-';
  $excludes //= {};

  _init_tension() if $Flag_Tension;

  my $ps_width = 24 - ( $Flag_Lyout ? 0 : 6 );

TRANS: for my $i ( 0 .. $Atu->scale_degrees - 1 ) {
    my %tps;
    @tps{ @{ $Atu->transpose( $i, $ps_base ) } } = ();
    if ( all { exists $tps{$_} } @$desired ) {
      my @pitches = @{ $Atu->transpose( $i, $ps_base ) };
      next if defined $root_pitch and $pitches[0] != $root_pitch;
      if (%$excludes) {
        for my $p (@pitches) {
          next TRANS if exists $excludes->{$p};
        }
      }

      my $tstr = '';
      if ($Flag_Tension) {
        $tstr = sprintf "\t%.03f  %.03f  %.03f",
          $Tension->vertical( \@pitches );
      }

      @pitches = $Lyu->p2ly(@pitches) if $Flag_Lyout;

      printf "%s\tT(%d)\t%-${ps_width}s%s\n", $fnum, $i,
        join( ',', @pitches ), $tstr;
    }
  }

TRANSINV: for my $i ( 0 .. $Atu->scale_degrees - 1 ) {
    my %ips;
    @ips{ @{ $Atu->transpose_invert( $i, 0, $ps_base ) } } = ();
    if ( all { exists $ips{$_} } @$desired ) {
      my @pitches = @{ $Atu->transpose_invert( $i, 0, $ps_base ) };
      next if defined $root_pitch and $pitches[0] != $root_pitch;
      if (%$excludes) {
        for my $p (@pitches) {
          next TRANSINV if exists $excludes->{$p};
        }
      }

      my $tstr = '';
      if ($Flag_Tension) {
        $tstr = sprintf "\t%.03f  %.03f  %.03f",
          $Tension->vertical( \@pitches );
      }

      @pitches = $Lyu->p2ly(@pitches) if $Flag_Lyout;

      printf "%s\tTi(%d)\t%-${ps_width}s%s\n", $fnum, $i,
        join( ',', @pitches ), $tstr;
    }
  }
}

sub fnums {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
  _init_tension('cope') if $Flag_Tension;

  my $fns = $Atu->fnums;
  for my $fn ( sort keys %$fns ) {
    my $pset = $fns->{$fn};
    my $icc  = $Atu->interval_class_content($pset);

    my $tstr = '';
    if ($Flag_Tension) {
      $tstr = sprintf "\t%.03f  %.03f  %.03f", $Tension->vertical($pset);
    }

    printf "%s\t%-16s\t%-8s%s\n", $fn, join( ',', @$pset ),
      join( '', @$icc ), $tstr;
  }
}

sub forte2pcs {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) or print_help();

  emit_pitch_set( $Atu->forte2pcs( $args[0] ), rs => $Flag_Record_Sep );
}

sub freq2pitch {
  my (@args) = @_;
  my $mode = 'absolute';
  GetOptionsFromArray(
    \@args,
    @Std_Opts,
    'concertfreq|cf=s'  => \my $concert_freq,
    'concertpitch|cp=s' => \my $concert_pitch,
    'relative=s'        => \my $relative,
    'scala=s'           => \my $scala_file,
  ) or print_help();

  my $scala = _init_scala( $concert_freq, $concert_pitch, $scala_file );

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

  # Not the default, so if things persist or chain due to some rewrite,
  # would need to save the old or create a new object or whatever
  $Lyu->keep_state(1);
  $Lyu->mode('absolute');

  for my $freq ( grep looks_like_number $_, map { split ' ', $_ } @args ) {
    die "frequency '$freq' out of range" if $freq < 8 or $freq > 4200;

    my $p = $scala->freq2pitch($freq);
    $p = $Lyu->p2ly($p) if $Flag_Lyout;
    printf "%.2f\t%s\n", $freq, $p;
  }
}

sub interval_class_content {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts );
  emit_pitch_set(
    scalar $Atu->interval_class_content( args2pitchset(@args) ),
    lyflag => 0,
    rs     => '',
  );
}

sub intervals2pcs {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts, 'pitch|n=s' => \my $start_pitch )
    or print_help();
  $start_pitch = $Lyu->notes2pitches( $start_pitch // 0 );

  $Lyu->ignore_register(0);

  emit_pitch_set( $Atu->intervals2pcs( $start_pitch, args2pitchset(@args) ),
    rs => $Flag_Record_Sep );
}

sub invariance_matrix {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts );
  emit_pitch_set(
    $Atu->invariance_matrix( args2pitchset(@args) ),
    lyflag => 0,
    rs     => $Flag_Record_Sep
  );
}

sub invariants {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
  $Lyu->chrome('flats') if $Flag_Flat;
  my $ps = args2pitchset(@args);

  my %seen;
  @seen{@$ps} = ();

  my $icc = scalar $Atu->interval_class_content($ps);
  warn '[',
    join( ',', map { $Flag_Lyout ? $Lyu->p2ly($_) : $_ } @$ps ),
    '] icc ', join( '', @$icc ), "\n";

  my $ps_len = 2 + @$ps * ( $Flag_Lyout ? 3 : 2 );

  for my $t ( 0 .. $Atu->scale_degrees - 1 ) {
    my $tps = $Atu->transpose( $t, $ps );
    my @t_invary;
    for my $p (@$tps) {
      push @t_invary, $p if exists $seen{$p};
    }
    if (@t_invary) {
      my $fn = $Atu->pcs2forte( \@t_invary );
      printf "%-6s [ %-${ps_len}s ] %s [ %-${ps_len}s ]", "T($t)",
        join( ',', map { $Flag_Lyout ? $Lyu->p2ly($_) : $_ } @$tps ),
        'ivars',
        join( ',', map { $Flag_Lyout ? $Lyu->p2ly($_) : $_ } @t_invary );
      print ' ', $fn if defined $fn;
      print "\n";
    }
  }

  for my $t ( 0 .. $Atu->scale_degrees - 1 ) {
    my $ips = $Atu->transpose_invert( $t, 0, $ps );
    my @i_invary;
    for my $p (@$ips) {
      push @i_invary, $p if exists $seen{$p};
    }
    if (@i_invary) {
      my $fn = $Atu->pcs2forte( \@i_invary );
      printf "%-6s [ %-${ps_len}s ] %s [ %-${ps_len}s ]", "Ti($t)",
        join( ',', map { $Flag_Lyout ? $Lyu->p2ly($_) : $_ } @$ips ),
        'ivars',
        join( ',', map { $Flag_Lyout ? $Lyu->p2ly($_) : $_ } @i_invary );
      print ' ', $fn if defined $fn;
      print "\n";
    }
  }
}

sub invert {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts, 'axis|n=s' => \my $axis, )
    or print_help();
  $Lyu->chrome('flats') if $Flag_Flat;
  $axis = defined $axis ? $Lyu->notes2pitches($axis) : 0;
  emit_pitch_set( $Atu->invert( $axis, args2pitchset(@args) ),
    rs => $Flag_Record_Sep );
}

sub ly2pitch {
  my (@args) = @_;
  my $mode = 'absolute';
  GetOptionsFromArray( \@args, @Std_Opts, 'relative=s' => \my $relative, )
    or print_help();

  $Lyu->ignore_register(0);
  $Lyu->keep_state(1);
  $Lyu->sticky_state(1);
  if ($relative) {
    $Lyu->mode('relative');
    $Lyu->prev_note($relative);
  } else {
    $Lyu->mode('absolute');
  }

  my @notes;
  if ( !@args or ( @args == 1 and $args[0] eq '-' ) ) {
    chomp( @args = readline STDIN );
  }

  # split input, as lilypond ' really do not suit the Unix shell, so
  # are best enclosed in "" blocks
  for my $arg (@args) {
    push @notes, split ' ', $arg;
  }

  if ( !@notes ) {
    die "Usage: $0 ly2pitch [--relative=note] [-|notes...]\n";
  }

  print join( ' ', $Lyu->notes2pitches(@notes) ), "\n";
}

sub multiply {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts, 'factor|n=s' => \my $factor, )
    or print_help();
  $Lyu->chrome('flats') if $Flag_Flat;
  $factor //= 1;
  die "factor must be number\n" unless looks_like_number $factor;
  emit_pitch_set( $Atu->multiply( $factor, args2pitchset(@args) ) );
}

sub normal_form {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
  $Lyu->chrome('flats') if $Flag_Flat;
  emit_pitch_set( ( $Atu->normal_form( args2pitchset(@args) ) )[0],
    rs => $Flag_Record_Sep );
}

sub notes2time {
  my (@args) = @_;
  my $beats  = 4;
  my $tempo  = 60;
  GetOptionsFromArray(
    \@args,
    'beats=i' => \$beats,
    'ms!'     => \my $in_ms,
    'tempo=i' => \$tempo,
  ) or print_help();

  my @durations;
  for my $notespec (@args) {
    if ( $notespec =~ m/(\d+)([.]+)?/ ) {
      my $dur = $beats / $1;
      my $dots = defined $2 ? length $2 : 0;
      push @durations, 2 * $dur - ( $dur / 2**$dots );
    } else {
      die "unable to parse duration from '$notespec'\n";
    }
  }

  my $beat_ms = bpm_to_ms( $tempo, $beats );
  for my $d (@durations) {
    my $d_ms = $d * $beat_ms;
    print( ( $in_ms ? $d_ms : _ms2abbr_time($d_ms) ), "\n" );
  }

  if ( @durations > 1 ) {
    my $total_ms = sum(@durations) * $beat_ms;
    print( '= ' . ( $in_ms ? $total_ms : _ms2abbr_time($total_ms) ), "\n" );
  }
}

sub pcs2forte {
  my (@args) = @_;
  my $fn = $Atu->pcs2forte( args2pitchset(@args) ) || "";
  print $fn, "\n";
}

sub pcs2intervals {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) or print_help();

  $Lyu->ignore_register(0);

  emit_pitch_set(
    $Atu->pcs2intervals( args2pitchset(@args) ),
    lyflag => 0,
    rs     => $Flag_Record_Sep,
  );
}

sub pitch2freq {
  my (@args) = @_;
  my $mode = 'absolute';
  GetOptionsFromArray(
    \@args,
    'concertfreq|cf=s'  => \my $concert_freq,
    'concertpitch|cp=s' => \my $concert_pitch,
    'relative=s'        => \my $relative,
    'scala=s'           => \my $scala_file,
  ) or print_help();

  my $scala = _init_scala( $concert_freq, $concert_pitch, $scala_file );

  $Lyu->ignore_register(0);
  $Lyu->keep_state(1);
  $Lyu->sticky_state(1);
  if ($relative) {
    $Lyu->mode('relative');
    $Lyu->prev_note($relative);
  } else {
    $Lyu->mode('absolute');
  }

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

  # If pitch must int() it, otherwise feed to lilypond for conversion so
  # do not first need to call ly2pitch on the input.
  for my $pitch (
    map { looks_like_number $_ ? int $_ : $Lyu->notes2pitches($_) }
    map { split ' ', $_ } @args
    ) {
    die "pitch '$pitch' out of range\n" if $pitch < 0 or $pitch > 108;
    printf "%d\t%.2f\n", $pitch, $scala->pitch2freq($pitch);
  }
}

sub pitch2intervalclass {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
  $Lyu->chrome('flats') if $Flag_Flat;
  die "$0 pitch2intervalclass pitch\n"
    unless defined $args[0] and $args[0] =~ m/^\d+$/;
  print $Atu->pitch2intervalclass( $args[0] ), "\n";
}

sub pitch2ly {
  my (@args) = @_;
  my $mode = 'absolute';
  GetOptionsFromArray( \@args, @Std_Opts, 'mode=s' => \$mode, )
    or print_help();

  # Not the default, so if things persist or chain due to some rewrite,
  # would need to save the old or create a new object or whatever
  $Lyu->keep_state(1);

  $Lyu->mode($mode) if defined $mode;

  my @pitches;
  if ( !@args or ( @args == 1 and $args[0] eq '-' ) ) {
    chomp( @args = readline STDIN );
    for my $arg (@args) {
      push @pitches, split ' ', $arg;
    }
  } else {
    @pitches = @args;
  }

  print join( ' ', $Lyu->p2ly(@pitches) ), "\n";
}

sub prime_form {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
  $Lyu->chrome('flats') if $Flag_Flat;
  emit_pitch_set( $Atu->prime_form( args2pitchset(@args) ),
    rs => $Flag_Record_Sep );
}

sub print_help {
  warn <<"END_USAGE";
Usage: $PROG_NAME [options] mode mode-args

Atonal music analysis utilities. Options:

  --flats               Show flats instead of sharps when --ly used.
  --help                Print this message.
  --listmodes           Show available modes (see Music::AtonalUtil for docs).
  --ly                  Show lilypond note names instead of pitch numbers.
  --scaledegrees=n      Set a custom number of scale degrees (default: 12).

Most modes accept a pitch set (a list of positive integers or lilypond
note names (see source for supported names)) either as arguments, or
specified on STDIN if the arguments list is blank, or the final argument
is a hyphen. Exceptions include:

  invert    --n=N       Custom inversion axis (default is 0).
  multiply  --n=N       Multiply the pitch set by a factor (default is 1).
  pitch2intervalclass   Accepts a single pitch, not a pitch set.
  transpose --n=N       Custom transposition (default is 0).

Forte Numbers should be usable anywhere a pitch set can be specified.
The output will vary depending on the mode, and may include Cope
tension numbers.

Example:
  $0 invert --axis=3  0 3 6 7

The following require two pitch sets; specify the pitch sets on STDIN
(one per line) instead of in the arguments:

  variances        Emits three lines: the intersection, the difference,
                   and the union of the supplied pitch sets.
  zrelation        Emits 1 if pitch sets zrelated, 0 if not.

Example:
  (echo 0,1,3,7; echo 0,1,4,6) | $0 zrelation

There is also a 'basic' mode that computes both the prime form and
interval class content (and Forte Number, if possible):

  $0 --ly basic c e g

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

END_USAGE
  exit 64;
}

sub recipe {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts, 'file=s' => \my $rfile )
    or print_help();
  $Lyu->chrome('flats') if $Flag_Flat;

  my $ps  = args2pitchset(@args);
  my $wps = [@$ps];

  open my $fh, '<', $rfile or die "could not open '$rfile': $!\n";
  eval {
    while ( my ( $method, @margs ) = split ' ', readline $fh ) {
      next if !$method or $method =~ m/^[\s#]/;
      chomp @margs;
      die "not a ", ref $Atu, " method" unless $Atu->can($method);
      $wps = $Atu->$method( @margs, $wps );
    }
  };
  if ($@) {
    chomp $@;
    die "recipe error at '$rfile' line $.: $@\n";
  }
  emit_pitch_set( $wps, rs => $Flag_Record_Sep );
}

sub retrograde {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
  $Lyu->chrome('flats') if $Flag_Flat;
  emit_pitch_set( $Atu->retrograde( args2pitchset(@args) ),
    rs => $Flag_Record_Sep );
}

sub rotate {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts, 'rotate|n=s' => \my $r, )
    or print_help();
  $Lyu->chrome('flats') if $Flag_Flat;
  $r //= 0;
  die "rotate must be number\n" unless looks_like_number $r;
  emit_pitch_set( $Atu->rotate( $r, args2pitchset(@args) ),
    rs => $Flag_Record_Sep );
}

sub set_complex {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
  $Lyu->chrome('flats') if $Flag_Flat;
  emit_pitch_set( $Atu->set_complex( args2pitchset(@args) ),
    rs => $Flag_Record_Sep );
}

sub subsets {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts, 'length|len=i' => \my $l, )
    or print_help();
  $Lyu->chrome('flats') if $Flag_Flat;
  $l //= -1;
  emit_pitch_set( $Atu->subsets( $l, args2pitchset(@args) ),
    rs => $Flag_Record_Sep );
}

sub stdin2pitchsets {
  my @ss;
  while ( my $line = readline STDIN ) {
    my @pset;
    if ( $line =~ m/($FORTE_NUMBER_RE)/ ) {
      @pset = @{ $Atu->forte2pcs($1) };
      die "unknown Forte Number '$1'\n" if !@pset;
    } else {
      for my $p ( $line =~ /([-\d\w]+)/g ) {
        push @pset, $Lyu->notes2pitches($p);
      }
    }
    push @ss, \@pset;
  }

  return \@ss;
}

sub tcs {
  my (@args) = @_;
  emit_pitch_set( $Atu->tcs( args2pitchset(@args) ), lyflag => 0 );
}

sub tcis {
  my (@args) = @_;
  emit_pitch_set( $Atu->tcis( args2pitchset(@args) ), lyflag => 0 );
}

sub tension {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) or print_help();

  $Flag_Tension = 'cope' unless defined $Flag_Tension;
  _init_tension();

  my ( $t_avg, $t_min, $t_max, $t_ref ) =
    $Tension->vertical( args2pitchset(@args) );
  printf "%.03f  %.03f  %.03f\t%s\n", $t_avg, $t_min, $t_max,
    join( ',', @$t_ref );
}

sub transpose {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts, 'transpose|n=s' => \my $t, )
    or print_help();
  $Lyu->chrome('flats') if $Flag_Flat;
  $t //= 0;

  my $pset = args2pitchset(@args);

  # if a number, transpose by that; if note, transpose to that note
  if ( !looks_like_number($t) ) {
    $t = $Lyu->notes2pitches($t) - $pset->[0];
  }
  emit_pitch_set( $Atu->transpose( $t, $pset ), rs => $Flag_Record_Sep );
}

sub transpose_invert {
  my (@args) = @_;
  GetOptionsFromArray(
    \@args, @Std_Opts,
    'axis|a=s'      => \my $axis,
    'transpose|t=s' => \my $t,
  ) or print_help();
  $Lyu->chrome('flats') if $Flag_Flat;

  my $pset = args2pitchset(@args);

  $axis = defined $axis ? $Lyu->notes2pitches($axis) : 0;

  # if a number, transpose by that; if note, transpose to that note
  $t //= 0;
  if ( !looks_like_number($t) ) {
    $t = $Lyu->notes2pitches($t) - $pset->[0];
  }

  emit_pitch_set( $Atu->transpose_invert( $t, $axis, $pset ),
    rs => $Flag_Record_Sep );
}

sub variances {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
  $Lyu->chrome('flats') if $Flag_Flat;
  emit_pitch_set( [ $Atu->variances( @{ stdin2pitchsets() } ) ],
    rs => $Flag_Record_Sep );
}

sub whatscalesfit {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
  $Lyu->chrome('flats') if $Flag_Flat;

  my $pset = args2pitchset(@args);
  for my $p (@$pset) {
    $p %= $scale_degrees;
  }

  my @scales =
    qw/major dorian phrygian lydian mixolydian aeolian locrian blues/;
  push @scales, "harmonic minor", "melodic minor", "hungarian minor";

  for my $scale (@scales) {
    my @asc = get_scale_nums($scale);
    _fit_scale( $scale, $pset, \@asc );

    my @dsc = get_scale_nums( $scale, 1 );
    if ( join( ' ', @asc ) ne join( ' ', reverse @dsc ) ) {
      _fit_scale( $scale, $pset, \@dsc, 1 );
    }
  }
}

sub zrelation {
  emit_pitch_set( [ $Atu->zrelation( @{ stdin2pitchsets() } ) ],
    lyflag => 0 );
}

END {
  # Report problems when writing to stdout (perldoc perlopentut)
  unless ( close(STDOUT) ) {
    die "error: problem closing STDOUT: $!\n";
  }
}

__END__

=head1 NAME

atonal-util - routines for atonal composition and analysis

=head1 SYNOPSIS

Prime form and APIC vector for a pitch set:

  $ atonal-util basic --ly f fis c

Apply a series of transformations to a pitch set:

  $ cat rules
  retrograde
  invert 6
  transpose 1
  $ atonal-util recipe --file=rules 0 11 3
  4,8,7

Among many other options.

=head1 DESCRIPTION

Routines for atonal music composition and analysis, plus other music
theory or composition related tasks. Global options and a mode must be
supplied, followed by any mode specific arguments. Most modes accept
pitch sets, though some perform other tasks and thus expect various
input formats. Pitch sets can be read as arguments or from standard
input; some modes require two pitch sets that must be supplied one per
line on standard input.

The output will vary depending on the mode, and may include Cope tension
numbers (average, min, max tension for the pitch set). Other programs
should not be written to use this program, as the output may change. Use
the underlying code or modules called by this program, as this program
is intended for interactive use.

=head1 OPTIONS

This program currently supports the following global command line
switches:

=over 4

=item B<--flats> | B<--noflats>

Uses flats instead of sharps in output (but only with B<--ly>). Specify
B<--noflats> to disable flats, in the event you have an alias that sets
B<--flats> by default. There is currently no B<--sharps> option, sorry.

Doubleflats or doublesharps are not supported in the output, as they
will be rendered to the underlying note (e.g. for C<Fx> the output
would be C<G>). Doubleflats or doublesharps can be specified in the
input, though.

=item B<--help>

Displays help and exits the program.

=item B<--listmodes>

Displays supported operation modes and exits the program.

=item B<--ly>

Show lilypond note names instead of raw pitch numbers.

=item B<--scaledegrees>

Adjust the number of scale degrees (default: 12). May cause strange,
untested results from various calculations, use with caution.

=back

=head1 MODES

Most modes accept a pitch set. These comprise raw pitch numbers
(integers) or lilypond note names (bis, c, des, etc.) specified either
on the command line or via standard input, though some modes have other
conventions or input they accept.

Various global flags (listed above) can be specified as options to modes
where they make sense.

=over 4

=item B<basic> I<pitch_set>

Shows the B<prime_form> and B<interval_class_content>.

=item B<circular_permute>

All permutations by rotation of the input pitch set. See
L<Music::AtonalUtil> for details.

=item B<combos> I<freq1> I<freq2> [I<freq3> ...]

Shows the combination tones of the input frequencies (or with the
optional I<--pitches> flag, MIDI pitch numbers or lilypond note names)
relative to the first listed frequency. The delta of equal temperament
tuning from the actual combination tone is also shown.

  $ atonal-util --ly combos 220 440
  $ atonal-util --ly combos --pitches "c' g'"

L<https://en.wikipedia.org/wiki/Combination_tone>

This mode also accepts the I<--concertfreq> or I<--cf> argument to
specify a different reference pitch than 440 Hz. This will affect both
notes input (different frequencies) and output (how error amounts for
how far the resulting combination tone is from the nearest pitch).

=item B<complement>

Emits those pitches not listed in the input. See L<Music::AtonalUtil>.

=item B<equivs>

Lists equivalents of the input pitch set under various transposition and
inverse transposition operations.

=item B<findall> I<--exclude=p1[,p2]> I<--fn=nums> I<--root=pitch> I<pitches>

Find all Forte pitch sets in which the given pitches exist. Like
B<findin>, except iterates over all Forte pitch sets instead of just a
named pitch set.

With I<--exclude>, omits results containing the listed pitches. With
I<--fn>, limits the search to the mentioned forte number prefixes (the
number of pitches in the set). With I<--root>, limits matches to those
with the named root pitch.

  $ atonal-util findall --root=c --fn=4-5 c e g bes

=item B<findin> I<--exclude=p1[,p2]> I<--root=pitch> I<--pitchset=base_set> I<pitches>

Answers questions such as, given a base pitch set of C<[0,3,7]>, and the
notes d and bes, what pitch sets (via any B<transpose> or
B<transpose_invert> operation) complete the base pitch set. With I<--
exclude>, omits results containing the listed pitches. With I<--root>,
limits matches to those with the named root pitch.

  $ atonal-util findin --exclude=c,ees --pitchset=5-25 d fis a

=item B<fnums> [ I<--tension=cope> ]

Returns a list of all Forte Numbers and corresponding pitch sets (and
their B<interval_class_content>). With the I<--tension> flag, also shows
Cope tension values for the pitch sets.

=item B<forte2pcs> I<forte_number>

Given a Forte Number, returns the corresponding pitch set.

=item B<freq2pitch> I<frequencies...>

Converts the listed frequencies (in Hz) into the closest MIDI note
numbers. Reads frequencies line-per-line from standard input if the only
frequency given is a C<->. With I<--ly> also converts the MIDI note
number to a lilypond note name.

This mode also accepts the I<--concertfreq> or I<--cf> argument to
specify a different reference pitch than 440 Hz.

=item B<interval_class_content>

See L<Music::AtonalUtil>, or
https://en.wikipedia.org/wiki/Interval_vector

=item B<intervals2pcs> I<--pitch=startpitch>

Converts a list of intervals to a pitch set. The default starting
pitch is 0.

=item B<invariance_matrix>

Invariance under Transpose(N)Inversion operation matrix. See
L<Music::AtonalUtil>.

=item B<invariants>

Returns list of B<transpose> or B<transpose_invert> operations that have
invariant pitches with the supplied pitch set, along with which pitches
have not varied.

=item B<invert> I<--axis=inversion_axis>

See L<Music::AtonalUtil>. Default axis is around pitch 0.

=item B<ly2pitch> I<--relative=note> notes...

Converts lilypond note names to pitch numbers, via absolute mode by
default. The input unlike in other modes may be quoted to protect the
lilypond C<'> register change from shell quoting rules. Example usages:

  $ echo c e g | atonal-util ly2pitch -
  $ atonal-util ly2pitch "c d' e f, g"
  $ atonal-util ly2pitch --relative=a\' a c d b

=item B<multiply> I<--factor=N>

Multiplies specified pitches by specified factor (C<1> by default).

=item B<normal_form>

Returns the normal form (via the "packed from the right" method) of the
given pitch set.

=item B<notes2time> I<--ms> I<--tempo=bpm> I<--beats=b> notedurs

Converts note durations (for example C<4> for a quarter note or C<4.>
for a dotted quarter note) to time, which is shown in a condensed form,
unless I<--ms> is supplied, in which case the duration is shown in
milliseconds.

=item B<pcs2forte>

Given a pitch set, returns the corresponding Forte Number, if any.

=item B<pcs2intervals>

Given a pitch set of at least two elements, returns the list of
intervals between the pitches.

=item B<pitch2freq> I<pitches...>

Converts pitches to frequencies (in Hz) using the standard MIDI note
number conversion equation. Reads pitches (or lilypond notes) line-by-
line from standard input if the only pitch given is C<->.

This mode also accepts the I<--concertfreq> or I<--cf> argument to
specify a different reference pitch than 440 Hz.

=item B<pitch2intervalclass> I<pitch>

Return the interval class a given pitch belongs to.

=item B<pitch2ly> I<--mode=relative|absolute> I<pitches...>

Converts pitches (integers) to lilypond note names. Reads pitches line-by-
line from standard input if only pitch given is C<->. Use the I<--mode>
option to specify relative or absolute conversion.

=item B<prime_form>

Returns the prime form of the given pitch set.

=item B<recipe> I<--file=recipefile>

Apply a series of named operations from a batch file to a given pitch
set. One possibility for C<recipefile> contents might be:

  retrograde
  invert 6
  transpose 1

=item B<retrograde>

Reverses the given list of pitches.

=item B<rotate> I<--rotate=integer>

Rotates the pitch set by the given integer (by default C<0> or no
rotation). L<Music::AtonalUtil> also has a B<rotateby> method to rotate
to a particular element that may be of use in composition.

=item B<set_complex>

Computes the set complex of a pitch set (a table of the pitch set, pitch
set inversion, and their combination).

=item B<subsets> [ I<--length=integer> ]

See L<Music::AtonalUtil>. The length, if supplied, must be a magnitude
equal to or less than the number of pitches supplied, and probably also
two or higher.

=item B<tcs>

Transposition common-tone structure. See L<Music::AtonalUtil>. Probably
easier to use the B<invariants> mode to list the full results.

=item B<tcis>

As above method except with B<transpose_invert> instead of B<transpose>.
Probably easier to use the B<invariants> mode to list the full results.

=item B<tension> I<pitch_set>

Returns the average, min, max, and tension values for all the tensions
in the passed pitch set, from the first notes in the set up to the last.
Tensions calculated via L<Music::Tension::Cope>.

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

Transposes the supplied pitches by the specified integer (by default 0,
or a no-op), or to the specified note.

  $ atonal-util transpose --transpose=4 --ly c e g
  e gis b
  $ atonal-util transpose --transpose=e --ly c e g
  e gis b

Register aware transpositions are possible with the C<canonical> tool of
the L<App::MusicTools> module, among many other nifty features (modal
transpositions, etc).

=item B<transpose_invert> I<--transpose=integer_or_note> [ I<--axis=integer_or_note> ]

Inverts and then transposes the supplied pitch set by the specified
integer, or to the specified note. Default axis for inversion is 0 (c).

=item B<variances>

Accepts two pitch sets, one per line, via standard input. Returns
three lines consiting of the intersection, difference, and union of
the given sets.

=item B<whatscalesfit>

Given a list of pitches or notes, shows what scales, if any, fit those
notes. Only diatonics of the scale are considered; there is no support
at present for also matching on bVI or other common chromatic
alterations.

=item B<zrelation>

Accepts two pitch sets, one per line, via standard input. Emits C<1>
if the two sets share the same B<interval_class_content>, C<0> if
they do not.

=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>

=head2 Known Issues

Poor naming conventions and standards of underlying music theory and any
associated mistakes in understanding thereof by the author.

=head1 SEE ALSO

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

L<Music::AtonalUtil>, L<Music::LilyPondUtil>, L<Music::Tension::Cope>

=head1 AUTHOR

Jeremy Mates

=head1 COPYRIGHT

Copyright (C) 2012-2013 by Jeremy Mates

This program 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
