#!/usr/bin/perl
#
# Accepts various inputs, applies Music::Chord::Positions functions to
# that input, outputs lilypond data containing the results. Example:
#
# mcp2ly inv 0 4 7 > inv.ly && lilypond inv.ly
# mcp2ly -f 0.1 -x -v 4 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,           %params,
  %pitch2note,      %registers,       $LY_DEFAULT_DURATION,
  $chord_spec,      $cn,              $duration,
  $exit_status,     $flats,           $fudge,
  $is_conventional, $ly_lower_voices, $ly_upper_voices,
  $mode,            $omit_orig,       $pitch_set,
  $reverse,         $shuffle,         $transpose,
  $voice_count,
);

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

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

%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 = (
  -3 => ",,,",
  -2 => ",,",
  -1 => ",",
  0  => "",
  1  => "'",
  2  => "''",
  3  => "'''",
  4  => "''''",
  5  => "'''''"
);

%modes = (
  inv => \&chord_inv,
  voc => \&chord_pos,
);

GetOptions(
  'conventional|c'  => \$is_conventional,
  'duration|D=s'    => \$duration,
  'exclude-orig|x'  => \$omit_orig,
  'flats'           => \$flats,
  'fudge|f=s'       => \$fudge,
  'param|p=s'       => \%params,
  'reverse|r'       => \$reverse,
  'shuffle|s'       => \$shuffle,
  'transpose|t=s'   => \$transpose,
  'voice-count|v=s' => \$voice_count,
);
$mode = shift;

if ( !defined $mode or !exists $modes{$mode} or !@ARGV ) {
  print_help();
  exit 64;
}

if ($flats) {
  %pitch2note =
    qw( 0 c 1 des 2 d 3 ees 4 e 5 f 6 ges 7 g 8 aes 9 a 10 bes 11 b );
}

$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) ] };
  if ($@) {
    warn
      "Music::Chord::Note could not parse '$chord_spec', see list in module src\n";
    exit 64;
  }
} 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 );

  if ($is_conventional) {
    my @ep = grep { $_ ne 'voice_count' } keys %params;
    delete @params{@ep};
    $params{'allow_transpositions'} = 1;
    $params{'no_partial_closed'}    = 1;
    $params{'pitch_max'}            = -1;
  }

  if ( $voice_count and exists $params{'voice_count'} ) {
    die "error: voice_count specified twice??\n";
  } elsif ( $voice_count and $mode eq 'voc' ) {
    $params{voice_count} = $voice_count;
  }

  @chords = $modes{$mode}->( $pitch_set, %params );
  die "no chords generated" unless @chords;

  warn "notice: reverse and shuffle together? really?\n"
    if $shuffle and $reverse;

  unshift @chords, $pitch_set unless $omit_orig;
  @chords = shuffle @chords if $shuffle;
  @chords = reverse @chords if $reverse;
  @voices = chords2voices(@chords);

  for my $voice (@voices) {
    my ( @registers, $mean_reg_num );
    for my $pitch (@$voice) {
      $pitch += $transpose if $transpose;
      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";
  }
}

$exit_status = print <<"END_TMPL";
\\version "2.12.0"

#(define-markup-list-command (paragraph layout props args) (markup-list?)
 (interpret-markup-list layout props
   (make-justified-lines-markup-list (cons (make-hspace-markup 2) args))))

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

upper = {
  \\clef treble

  $ly_upper_voices
}

lower = {
  \\clef bass

  $ly_lower_voices
}

\\markuplines { \\paragraph {
@orig_args
} }

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

END {
  unless ( close(STDOUT) ) {
    warn "error: problem closing STDOUT: $!\n";
    exit 74;
  }
}

exit $exit_status;

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

sub print_help {
  warn <<"END_USAGE";
Usage: $0 [options] inv|voc

Generates chord inversions or voicings in lilypond format to stdout.

Options:

  conventional     Set a bunch of parameters for "conventional" voicings.
  duration|D=s     Specify custom duration for chords generated (default whole)
  exclude-orig|x   Exclude original chord from output.
  flats            Use flats instead of sharps in output.
  fudge|f=s        Fudge factor for what clef voices end up in.
  param|p=s        Custom parameters, see Music::Chord::Positions docs.
  reverse|r        Reverse order of the generated chords.
  shuffle|s        Shuffle chords randomly.
  transpose|t=s    Value in semitones to transpose output by.
  voice-count|v=s  How many voices to generate.

END_USAGE
}
