package Tracks;

use Moose;
use Audio::FLAC::Header;
use IO::Lines;
use IO::File;
use Digest::SHA1;

# conversion factors
use constant FRAMES_PER_SECOND => 75;
use constant SECONDS_PER_MINUTE => 60;

# see also http://flac.sourceforge.net/format.html#cuesheet_track
# 588 samples/frame = 44100 samples/sec / 75 frames/sec
use constant SAMPLES_PER_FRAME => 588;

# XXX: does this every vary?
# or is the lead-in always 88200 samples (88200 / 588 = 150) i.e. 2 seconds?
use constant SECTOR_OFFSET => 150;

has tracks => (
    is      => 'rw',
    default => sub { [] },
);

has discid => (
    is       => 'ro',
    builder  => '_calculate_musicbrainz_discid',
    lazy     => 1,
    init_arg => undef,
);

sub _get_tracks_from_cdinfo {    
    my $device = shift;
    my @tracks;
    open my $CD_INFO, 'cd-info -q |' or die "Unable to run cd-info: $!";
    while (<$CD_INFO>) {
        next unless /^\s*([0-9]+): \d\d:\d\d:\d\d  (\d{6})/;
        my ($num, $sector) = ($1, $2);
        my $track = {
            number => $num,
            sector => $sector,
        };
        # place leadout track (170) at index 0
        $num != 170 ? $tracks[$num] = $track : $tracks[0] = $track;
    }
    close $CD_INFO;

    return @tracks;
}

sub _get_tracks_from_cdparanoia {
    my $device = shift;
    my @tracks;
    open my $CDP, 'cdparanoia -d ' . $device . ' -Q 2>&1 |' or die "Unable to run cdparanoia: $!";
    while (<$CDP>) {
        if (m{
            ^\s+(\d+)\.               # track number
            \s+(\d+)                  # length
            \s+\[(\d\d:\d\d\.\d\d)\]  # length (MSF)
            \s+(\d+)                  # start
            \s+\[(\d\d:\d\d\.\d\d)\]  # start (MSF)
        }x) {
            my ($track, $length, $length_msf, $start, $start_msf) = ($1, $2, $3, $4, $5);
            $start_msf =~ s/\./:/;
            $tracks[$track] = {
                number => $track,
                sector => $start,
                msf    => $start_msf,
            };
        } elsif (m{TOTAL\s+(\d+)}) {
            my $total = $1;
            my $leadout = $total + $tracks[1]{sector};
            $tracks[0] = {
                number => 170,
                sector => $leadout,
            };        
        }    
    }
    close $CDP;

    return @tracks;
}

sub read_disc {
    my ($self, $device) = @_;
    $self->tracks([ _get_tracks_from_cdparanoia($device) ]);
}

sub parse_cuesheet {
    my ($handle) = @_;

    my @tracks;
    my $track;
    while (<$handle>) {
        if (/TRACK (\d\d)/) {
            $track = int($1);
        } elsif (/INDEX 01/) {
            my ($m,$s,$f) = /INDEX 01 (\d\d):(\d\d):(\d\d)/;
            my $sector = ($m * SECONDS_PER_MINUTE * FRAMES_PER_SECOND) + ($s * FRAMES_PER_SECOND) + $f;
            $tracks[$track] = {
                number => $track,
                sector => $sector,
                msf    => "$m:$s:$f",
            };
        } elsif (/lead-out/) {
            my ($total_samples) = /lead-out \d+ (\d+)/;
            $tracks[0] = {
                number => 170,
                sector => $total_samples / SAMPLES_PER_FRAME,
            };
        }
    }
    return @tracks;
}

sub read_flac {
    my ($self, $file) = @_;

    my $flac = Audio::FLAC::Header->new($file);
    my $cuesheet_lines = $flac->cuesheet;
    my $CUE = IO::Lines->new($cuesheet_lines);

    $self->tracks([ parse_cuesheet($CUE) ]);
}

sub read_cue {
    my ($self, $file) = @_;

    my $CUE = $file eq '-' ? \*STDIN : IO::File->new($file, '<');
    $self->tracks([ parse_cuesheet($CUE) ]);
}

sub has_tracks {
    my $self = shift;
    return @{ $self->tracks } > 0;
}

# https://musicbrainz.org/doc/Disc_ID_Calculation
sub _calculate_musicbrainz_discid {
    my ($self) = @_;

    my @tracks = @{ $self->tracks };

    return unless @tracks;

    my $sha1 = Digest::SHA1->new;

    $sha1->add(sprintf('%02X', $tracks[1]{number}));
    $sha1->add(sprintf('%02X', $tracks[-1]{number}));
    for my $i (0 .. 99) {
        my $offset = (defined $tracks[$i]{sector} ? ($tracks[$i]{sector} + SECTOR_OFFSET) : 0);
        $sha1->add(sprintf('%08X', $offset));
    }

    my $digest = $sha1->b64digest;
    $digest =~ tr{+/=}{._-};
    $digest .= '-';  ## why do we need to manually add this?

    return $digest;
}

sub get_musicbrainz_tocdata {
    my ($self) = @_;
    my @tracks = @{ $self->tracks };
    # this is a CD TOC suitable for submitting to MusicBrainz as a CD Stub
    # http://musicbrainz.org/doc/XML_Web_Service#Submitting_a_CDStub
    return (
        # first track number
        $tracks[1]{number},
        # last track number
        $tracks[-1]{number},
        # last frame (sector?)
        $tracks[0]{sector} + SECTOR_OFFSET,
        # start frame for each track
        map { $_->{sector} + SECTOR_OFFSET } @tracks[1 .. @tracks - 1],
    );
}

sub get_cuesheet {
    my ($self) = @_;
    my @tracks = @{ $self->tracks };
    my @cuesheet;
    push @cuesheet, qq{FILE "cdda.wav" WAVE};
    for my $i (1 .. @tracks - 1) {
        my $track = $tracks[$i];
        push @cuesheet, sprintf('  TRACK %02d AUDIO', $i);
        if ($i == 1 && $track->{sector} != 0) {
            push @cuesheet, '    INDEX 00 00:00:00';
        }
        push @cuesheet, '    INDEX 01 ' . $track->{msf};
    }
    return join('', map { "$_\n" } @cuesheet);
}

# module return
1;
