#!/usr/bin/perl
#
# Accepts various inputs, applies Music::Chord::Positions functions to
# that input, outputs lilypond data containing the results. Example:
#
# mcp2ly -o inv 0 4 7 > inv.ly && lilypond inv.ly
# mcp2ly -f 0.1 -x -v 4 -o voc 0 4 7 > voc.ly && lilypond voc.ly
#
# Warning! Some pitch sets and looser restriction rules will result in
# massive numbers of voicings.

use strict;
use warnings;

use File::Basename qw/basename/;
use List::Util qw/shuffle sum/;
use Getopt::Long qw/GetOptions/;
use Music::Chord::Note ();
use Music::Chord::Positions qw/:all/;
use POSIX qw/floor/;

my (
  @orig_args,       %modes,               %pitch2note,
  %registers,       $LY_DEFAULT_DURATION, $chord_spec,
  $cn,              $duration,            $fudge,
  $ly_lower_voices, $ly_upper_voices,     $mode,
  $omit_orig,       $pitch_set,           $shuffle,
  $voice_count,
);

@orig_args = ( map { $_ =~ tr/"/'/; $_ } basename($0), @ARGV );

$duration = 1;      # whole note
$fudge    = 0.5;    # used to help figure out what staff a voice ends up in

%pitch2note =
  qw( 0 c 1 cis 2 d 3 dis 4 e 5 f 6 fis 7 g 8 gis 9 a 10 ais 11 b );
%registers =
  ( 0 => "", 1 => "'", 2 => "''", 3 => "'''", 4 => "''''", 5 => "'''''" );

%modes = (
  inv => sub { chord_inv( $_[0] ) },
  voc => sub {
    # TODO add means to specify other params on CLI
    chord_pos( $_[0], voice_count => $_[1], );
  }
);

GetOptions(
  'duration|D=s'     => \$duration,
  'exclude-orig|x'   => \$omit_orig,
  'fudge|f=s'        => \$fudge,
  'operation|op|o=s' => \$mode,
  'shuffle|R'        => \$shuffle,
  'voice-count|v=s'  => \$voice_count,
);

if ( !defined $mode or !exists $modes{$mode} or !@ARGV ) {
  die "Usage: $0 [-v voicecount] [-x] -o [inv|voc] pitch set spec\n";
}

$chord_spec = "@ARGV";

if ( $chord_spec =~ m/[A-Za-z()-]/ or $chord_spec =~ m/^\s*\d+\s*$/ ) {
  eval { $pitch_set = [ Music::Chord::Note->new->chord_num($chord_spec) ] };
  die
    "Music::Chord::Note could not parse '$chord_spec', see list in module src\n"
    if $@;
} else {
  my @pitches = $chord_spec =~ m/(\d+)/g;
  $pitch_set = \@pitches;
}

# Get voice lines, convert to lilypond format, figure out what staff the
# voices should be in. TODO messy, really should be templated or part of
# the module.
{
  my ( @chords, @voices, @uv, @lv );

  @chords =
    $modes{$mode}->( $pitch_set, ( $voice_count || scalar @$pitch_set ) );
  unshift @chords, $pitch_set unless $omit_orig;
  @chords = shuffle @chords if $shuffle;
  @voices = chords2voices(@chords);

  for my $voice (@voices) {
    my ( @registers, $mean_reg_num );
    for my $pitch (@$voice) {
      my $ly_pitch = $pitch2note{ $pitch % scale_deg() };
      my $reg_num  = int( $pitch / scale_deg() );
      push @registers, $reg_num;

      $pitch = $ly_pitch . $registers{$reg_num};
    }
    my $mean_register = floor( sum(@registers) / @$voice + $fudge );

    $voice->[0] .= $duration;
    if ( $mean_register > 0 ) {
      push @uv, join " ", @$voice;
    } else {
      push @lv, join " ", @$voice;
    }
  }

  if (@uv) {
    $ly_upper_voices = "<< {\n" . join( "\n} \\\\ {\n", @uv ) . "\n} >>\n";
  }
  if (@lv) {
    $ly_lower_voices = "<< {\n" . join( "\n} \\\\ {\n", @lv ) . "\n} >>\n";
  }
}

print <<"END_TMPL";
\\version "2.12.0"

\\header {
  title    = "Pitch set: @$pitch_set"
  subtitle = "@orig_args"
  subsubtitle = "Music::Chord::Positions v.$Music::Chord::Positions::VERSION"
}

upper = {
  \\clef treble

  $ly_upper_voices
}

lower = {
  \\clef bass

  $ly_lower_voices
}

\\score {
  \\new PianoStaff <<
    \\new Staff = "upper" \\upper
    \\new Staff = "lower" \\lower
  >>
  \\layout { }
  \\midi { }
}
END_TMPL
