/* 
 * A bit of music theory. Enough to draw a keyboard, do some basic typesetting,
 * and have something interesting to think about over Christmas.
 *
 * Copyright Joe Wass 2011 - 2012
 *
 * This isn't open source, but feel free to have a look around and even ask questions if you want.
 *
 * I talk about two kinds of pitch class (degree): diatonic and chromatic.
 * If you don't know what these mean, this will go over your head.
 *
 *
 */

var ACCIDENTAL = {
    SHARP: 's',
    FLAT: 'f',
    NATURAL: 'n'
};

var MIDDLE_C = 60;

// The order of preference for black note labelling (D sharp vs E flat) is based on only using black notes for minor scales.
var DIATONIC_DEGREES = {
    /* C       */ 0:  [{degree: 0, accidental: ACCIDENTAL.NATURAL}],
    /* C# / Db */ 1:  [{degree: 0, accidental: ACCIDENTAL.SHARP}, {degree: 1, accidental: ACCIDENTAL.FLAT}],
    /* D       */ 2:  [{degree: 1, accidental: ACCIDENTAL.NATURAL}],
    /* D# / Eb */ 3:  [{degree: 2, accidental: ACCIDENTAL.FLAT}, {degree: 1, accidental: ACCIDENTAL.SHARP}],
    /* E       */ 4:  [{degree: 2, accidental: ACCIDENTAL.NATURAL}],
    /* F       */ 5:  [{degree: 3, accidental: ACCIDENTAL.NATURAL}],
    /* F# / Gb */ 6:  [{degree: 3, accidental: ACCIDENTAL.SHARP}, {degree: 4, accidental: ACCIDENTAL.FLAT}],
    /* G       */ 7:  [{degree: 4, accidental: ACCIDENTAL.NATURAL}],
    /* G# / Ab */ 8:  [{degree: 4, accidental: ACCIDENTAL.SHARP}, {degree: 5, accidental: ACCIDENTAL.FLAT}],
    /* A       */ 9:  [{degree: 5, accidental: ACCIDENTAL.NATURAL}],
    /* A# / Bb */ 10: [{degree: 6, accidental: ACCIDENTAL.FLAT}, {degree: 5, accidental: ACCIDENTAL.SHARP}],
    /* B       */ 11: [{degree: 6, accidental: ACCIDENTAL.NATURAL}]
};

// Note names relative to Middle C.
var DIATONIC_NOTE_NAMES = ["C", "D", "E", "F", "G", "A", "B"];

// For a given pitch, return the position within the scale starting on the relativeTo.
// Both numbers are MIDI pitches.
// Return object of:
// chromaticDegree: chromatic degree within the scale.
// accidental: Natural or accidental
// diatonicDegree: Diatonic degree of scale (0 - 11)
// diatonicRelative: Diatonic degree of scale relative to note, may be positive or negative.
function positionRelativeToPitch(givenPitch, relativeTo)
{
    // We need to produce something with a valid absolute value when compared to relativeTo, 
    // which it wouln't if it had a lower value.
    // This seems a really stupid way to do it, but it works.
    // Am I being stupid?
    var absPitch = givenPitch;
    while (absPitch < relativeTo)
    {
        absPitch += 12;
    }
    
    // The degree within the scale, 0 to 11
    var degree = Math.abs((relativeTo-absPitch) % 12);
    
    
    // There are sharp/flat alternatives, but for now just get the first one that comes.
    var diatonicDegree = DIATONIC_DEGREES[degree][0].degree;
    var diatonicAccidental = DIATONIC_DEGREES[degree][0].accidental;

    var relativeOctave = Math.floor((givenPitch - relativeTo) / 12);
    var diatonicRelative = relativeOctave * 7 + diatonicDegree;
        
    return({
        chromaticDegree: degree,
        chromaticAbsolute: givenPitch,
        diatonicDegree: diatonicDegree,
        diatonicAccidental: diatonicAccidental,
        diatonicRelative: diatonicRelative
    });
}

function noteName(pitch)
{
    var contextualDegree = positionRelativeToPitch(pitch, MIDDLE_C);
    
    // Ignore sharp or flat hinting for now, take the first option.
    var name = DIATONIC_NOTE_NAMES[contextualDegree.diatonicDegree];
    
    
    if (contextualDegree.diatonicAccidental === ACCIDENTAL.SHARP)
    {
        name += "♯";
    }
    else if (contextualDegree.diatonicAccidental === ACCIDENTAL.FLAT)
    {
        name += "♭";
    }
    
    return name;
}

// Turn an element into a keyboard, with a callback called on each note.
function Keyboard($keyboardContainer, callback)
{
    var LOWEST_PITCH = 55;
    var HIGHEST_PITCH = 79;
    var WHITE_NOTE_WIDTH = 40;
    var BLACK_NOTE_WIDTH = 25;
    var BLACK_NOTE_OFFSET = WHITE_NOTE_WIDTH - (BLACK_NOTE_WIDTH / 2);
    var WHITE_NOTE_HEIGHT = 100;
    var BLACK_NOTE_HEIGHT = 50;
    
    var BLACK_HINT = 1;
    
    $keyboardContainer.css("height", WHITE_NOTE_HEIGHT);
    $keyboardContainer.parent().css("height", WHITE_NOTE_HEIGHT);
    
    var pitch;
    var x = 0;
    for (pitch = LOWEST_PITCH; pitch <= HIGHEST_PITCH; pitch++)
    {
        var $key = $("<div></div>");
        $key.css("position", "absolute");
        $key.css("top", 0);
        $key.css("cursor", "hand");
        
        var $key_label = $("<div></div>");
        $key_label.css("position", "absolute");
        $key_label.css("bottom", "0px");
        
        $key_label.css("-webkit-user-select", "none");
        $key_label.css("-khtml-user-select", "none");
        $key_label.css("-moz-user-select", "none");
        $key_label.css("-o-user-select", "none");
        $key_label.css("user-select", "none");        

        $key_label.css("text-align", "center");
        $key_label.css("cursor", "hand");
        
        var contextualDegree = positionRelativeToPitch(pitch, MIDDLE_C);
        if (contextualDegree.diatonicAccidental === ACCIDENTAL.NATURAL)
        {
            // White note
            
            x = x + WHITE_NOTE_WIDTH;
            
            $key.css("background-color", "white");
            $key.css("color", "black");
            $key.css("border", "1px solid #e0e0e0");
            $key.css("height", WHITE_NOTE_HEIGHT);
            $key.css("width", WHITE_NOTE_WIDTH);
            $key.css("left", x);
            $key.css("z-index", 1);
            
            $key_label.css("width", WHITE_NOTE_WIDTH);
        }
        else
        {
            // Black note
            
            $key.css("background-color", "black");
            $key.css("color", "white");
            $key.css("border", "1px solid #a0a0a0");
            $key.css("height", BLACK_NOTE_HEIGHT);
            $key.css("width", BLACK_NOTE_WIDTH);    

            $key.css("-webkit-border-bottom-right-radius", "4px");
            $key.css("-webkit-border-bottom-left-radius", "4px");
            $key.css("-moz-border-radius-bottomright", "4px");
            $key.css("-moz-border-radius-bottomleft", "4px");
            $key.css("border-bottom-right-radius", "4px");
            $key.css("border-bottom-left-radius", "4px");

            $key.css("z-index", 2);
            
            if (contextualDegree.chromaticDegree === 6 || contextualDegree.chromaticDegree === 1)
            {
                $key.css("left", (x + BLACK_NOTE_OFFSET) + BLACK_HINT);
            }
            else if (contextualDegree.chromaticDegree === 10 || contextualDegree.chromaticDegree === 3)
            {
                $key.css("left", (x + BLACK_NOTE_OFFSET) - BLACK_HINT);
            }
            else
            {
                $key.css("left", x + BLACK_NOTE_OFFSET);
            }
            
            $key_label.css("width", BLACK_NOTE_WIDTH);
        }

        (function(pitch)
        {
            $key.click(function(){callback.keyboard_click(pitch);});
            $key.hover(function(){callback.keyboard_hover(pitch);}, function(){callback.keyboard_unhover(pitch);});

            $key_label.html(noteName(pitch));
            $key.append($key_label);
        
            $keyboardContainer.append($key);
        })(pitch);
    }
    
}

function Stave($staveContainer, musick)
{
    this.$staveContainer = $staveContainer;
    this.musick = musick;
    
    // The MIDI value of the first line.
    // Hard-coded for treble clef. For now.
    var FIRST_LINE = 64;
    
    // The key signature of the stave.
    var KEY = 60;
    
    // Where to put the clef in terms of the position within the stave.
    var CLEF_NOTE = 4;
    
    // How many potential ledger lines will we be allowing? To allow for padding etc.
    // TODO yet!
    var LEDGER_LINES_ABOVE = 5;
    var LEDGER_LINES_BELOW = 5;

    // Five lines in the stave is this many semitones
    var SEMITONES_IN_STAVE_LINES = 14;

    // Size of a position in px. A line is made up of two positions.
    var PITCH_HEIGHT = 6.5;
    
    // Space between noteheads in px.
    var HORIZONTAL_SPACING = 20;
    
    var MAX_NOTES = 10;
    var MIN_NOTES = 5;

    this.overallHeight = ((LEDGER_LINES_ABOVE * 2) + (LEDGER_LINES_BELOW * 2) + SEMITONES_IN_STAVE_LINES) * (PITCH_HEIGHT / 2);

    // Array of pitches.
    this.pitches = [];
    
    // Optional pitch to display as hover.
    this.hoverPitch = null;
    this.$staveContainer.height(this.overallHeight);
    var containerHeight = this.overallHeight;
    
    // Set pitch. If position is null, append.
    this.setPitch = function(pitch, position)
    {
        if (!position)
        {
            
            if (this.pitches.length > MAX_NOTES)
            {
                alert("You can only have " + MAX_NOTES + " notes.");
                return;
            }
            
            this.pitches.push(pitch);
            return;
        }
        
        // Can't seek past end.
        if (position > this.pitches.length - 1)
        {
            return;
        }
        
        if (position > MAX_NOTES-1)
        {
            alert("You can only have " + MAX_NOTES + " notes.");
            return;
        }
        
        
        this.pitches[position] = pitch;
    };
    
    this.setHoverPitch = function(pitch)
    {
        this.hoverPitch = pitch;
    };
    
    this.render = function()
    {
        var ee = $("#id_title").val() === "cats";
        
        // The first line of the stave as an absolute diatonic value.
        var firstLineDiatonic = positionRelativeToPitch(KEY, FIRST_LINE).diatonicRelative;
        
        // Args to these are positions within the stave. They should return absolute coords.
        function notehead(x, stavePosition, hover)
        {
            var $head = $("<div style='position: absolute'></div>)");
            var width;
            var height;
            
            if (hover)
            {
                $head.css("background-image", "url('/static/img/notehead-hover.png')");
                width = 18;
                height = 16;
            }
            else if (!ee)
            {
                $head.css("background-image", "url('/static/img/notehead.png')");
                width = 16;
                height = 14;
            }
            else
            {
                $head.css("background-image", "url('/static/img/cat.png')");
                width = 20;
                height = 34;
            }

            var centreX = width/2;
            var centreY = height/2;
            
            $head.css("width", width);
            $head.css("height", height);
            
            $head.css("background-repeat", "no-repeat");
            
            $head.css("left", (x * HORIZONTAL_SPACING) - centreX);
            
            $head.css("top", (containerHeight - stavePosition * PITCH_HEIGHT) - centreY);
            
            return $head;
        }
        
        function sharp(x, stavePosition)
        {
            var $sharp = $("<div style='position: absolute;'></div>)");
            var width = 14;
            var height = 39;
            
            var centreX = width/2;
            var centreY = height/2;
            
            $sharp.css("background-image", "url('/static/img/sharp.png')");
            $sharp.css("width", width);
            $sharp.css("height", height);
            $sharp.css("left", (x * HORIZONTAL_SPACING) - centreX);
            $sharp.css("top", (containerHeight - stavePosition * PITCH_HEIGHT) - centreY);
            
            return $sharp;
        }

        function flat(x, stavePosition)
        {
            var $flat = $("<div style='position: absolute;'></div>)");
            var width = 11;
            var height = 31;
            
            var centreX = width/2;
            var centreY = height * 0.7;
            
            $flat.css("background-image", "url('/static/img/flat.png')");
            $flat.css("width", width);
            $flat.css("height", height);
            $flat.css("left", (x * HORIZONTAL_SPACING) - centreX);
            $flat.css("top", (containerHeight - stavePosition * PITCH_HEIGHT) - centreY);
            
            return $flat;
        }
        
        function natural(x, stavePosition)
        {
            var $natural = $("<div style='position: absolute;'></div>)");
            var width = 8;
            var height = 34;
            
            var centreX = width/2;
            var centreY = height/2;
            
            $natural.css("background-image", "url('/static/img/natural.png')");
            $natural.css("width", width);
            $natural.css("height", height);
            $natural.css("left", (x * HORIZONTAL_SPACING) - centreX);
            $natural.css("top", (containerHeight - stavePosition * PITCH_HEIGHT) - centreY);
            
            return $natural;
        }

        function trebleClef(x, stavePosition)
        {
            var $clef = $("<div style='position: absolute;'></div>)");
            var width = 32;
            var height = 99;
            
            var centreX = 0;
            var centreY = height * 0.65;
            
            $clef.css("background-image", "url('/static/img/treble-clef.png')");
            $clef.css("width", width);
            $clef.css("height", height);
            $clef.css("left", (x * HORIZONTAL_SPACING) - centreX);
            $clef.css("top", (containerHeight - stavePosition * PITCH_HEIGHT) - centreY);
            
            return $clef;
        }
        
        // Sort out a line for the given position. It may not need one.
        // stavePosition is position within the stave.
        // diatonicRelative is the diatonic relative to the key sig for working out accidentals.
        function line(x, stavePosition, diatonicRelative)
        {                        
            var needed = (diatonicRelative - firstLineDiatonic) % 2 === 1;
            
            if (!needed)
            {
                return null;
            }
            
            var $line = $("<div style='position: absolute;'></div>)");
            var width = 40;
            var height = 2;
            
            var centreX = width/2;
            var centreY = height/2;
            
            $line.css("background-color", "black");
            $line.css("width", width);
            $line.css("height", height);
            $line.css("left", (x * HORIZONTAL_SPACING) - centreX);
            $line.css("top", (containerHeight - stavePosition * PITCH_HEIGHT) - centreY);
            
            return $line;
        }
        
        function backspace(x, callback)
        {
            // var $bs = $("<div style='position: absolute;'>delete</div>)");

            var $bs = $("<button style='position: absolute; font-size: 20px;' class='button' >delete</button>)");

            var width = 43;
            var height = 34;
            
            $bs.click(callback);

            
            // $bs.css("background-image", "url('/static/img/backspace.png')");
            // $bs.css("background-repeat", "no-repeat");

            $bs.css("-webkit-user-select", "none");
            $bs.css("-khtml-user-select", "none");
            $bs.css("-moz-user-select", "none");
            $bs.css("-o-user-select", "none");
            $bs.css("user-select", "none");        
            
            // $bs.css("padding-left", width);
            $bs.css("vertical-align", "middle");
            $bs.css("height", height);
            $bs.css("left", x * HORIZONTAL_SPACING);
            $bs.css("top", (containerHeight / 2));
            
            return $bs;
        }
        
        function submitButton(x, callback)
        {
            var $button = $("<button style='position: absolute; font-size: 24px;' class='button' >Search</button>)");
            var width = 200;
            var height = 200;
            
            $button.click(callback);

            $button.css("left", 560);
            $button.css("top", (containerHeight / 2));
            
            return $button;
        }        

        this.$staveContainer.children().remove();
        
        var containerHeight = this.$staveContainer.height();
        
        // Take a copy so we can push the hoverpitch on the end without mucking things up.
        var display_pitches = this.pitches.slice();
        if (this.hoverPitch)
        {
            display_pitches.push(this.hoverPitch);
        }
        
        var $clef = trebleClef(0, CLEF_NOTE);
        this.$staveContainer.append($clef);
        
        // Lines for clef
        var i;
        var position;
        for (i = FIRST_LINE; i < FIRST_LINE + SEMITONES_IN_STAVE_LINES; i++)
        {
            position = positionRelativeToPitch(i, MIDDLE_C);
            var $line = line(x, position.diatonicRelative, position.diatonicRelative);
            
            this.$staveContainer.append($line);
        }
        
        // The position xwards. Depends on sharps and flats ekt.
        // Bit extra for the clef.
        var x = 3;
        
        // The current state of accidentals for a given diatonic pitch class.
        var accidentalState = [];
        for (i = 0; i < 7; i++)
        {
            accidentalState[i] = ACCIDENTAL.NATURAL;
        }
        
        var $sharp, $natural, $flat, $staveLine, lineI, $line;
        for (i = 0; i < display_pitches.length; i++)
        {
            var isLastPosition = i === (display_pitches.length - 1);
            var hoverNotehead = (isLastPosition && this.hoverPitch);
            
            var pitch = display_pitches[i];
            var contextualDegree = positionRelativeToPitch(pitch, MIDDLE_C);
            
            // Position relative to first line of stave.
            var stavePosition = contextualDegree.diatonicRelative;
            
            // And stave lines for this note.
            for (lineI = FIRST_LINE; lineI < FIRST_LINE + SEMITONES_IN_STAVE_LINES; lineI++)
            {
                position = positionRelativeToPitch(lineI, MIDDLE_C);
                $staveLine = line(x, position.diatonicRelative, position.diatonicRelative);
            
                this.$staveContainer.append($staveLine);
            }
            
            // And any required ledger lines.
            if (pitch < FIRST_LINE )
            {
                for (lineI = pitch; lineI < FIRST_LINE; lineI++)
                {
                    position = positionRelativeToPitch(lineI, MIDDLE_C);
                    $staveLine = line(x, position.diatonicRelative, position.diatonicRelative);

                    this.$staveContainer.append($staveLine);
                }
            }
            else if (pitch > FIRST_LINE + SEMITONES_IN_STAVE_LINES)
            {
                for (lineI = FIRST_LINE + SEMITONES_IN_STAVE_LINES; lineI <= pitch ; lineI++)
                {
                    position = positionRelativeToPitch(lineI, MIDDLE_C);
                    
                    $staveLine = line(x, position.diatonicRelative, position.diatonicRelative);

                    this.$staveContainer.append($staveLine);                    
                }
            }
            
            // Becuase we're getting accidentals involved, we need to keep track of sharps and flats per pitch class
            switch (contextualDegree.diatonicAccidental)
            {
                case ACCIDENTAL.FLAT:
                
                    switch (accidentalState[contextualDegree.diatonicDegree])
                    {
                        case ACCIDENTAL.FLAT:
                            // Nothing to see here.
                            break;

                        case ACCIDENTAL.NATURAL:
                            $flat = flat(x, stavePosition);
                            this.$staveContainer.append($flat);
                            x++;                        
                            break;

                        case ACCIDENTAL.SHARP:
                            $natural = natural(x, stavePosition);
                            this.$staveContainer.append($natural);
                            x++;

                            $flat = flat(x, stavePosition);
                            this.$staveContainer.append($flat);
                            x++;                        
                        
                        break;
                    }
                    break;
                
                case ACCIDENTAL.NATURAL:
                
                    switch (accidentalState[contextualDegree.diatonicDegree])
                    {
                        case ACCIDENTAL.FLAT:
                            $natural = natural(x, stavePosition);
                            this.$staveContainer.append($natural);
                            x++;
                            break;
                            
                        case ACCIDENTAL.NATURAL:

                            // Nothing to see here.
                            break;

                        case ACCIDENTAL.SHARP:
                            $natural = natural(x, stavePosition);
                            this.$staveContainer.append($natural);
                            x++;
                        break;
                    }
                    break;
                
                case ACCIDENTAL.SHARP:
                
                    switch (accidentalState[contextualDegree.diatonicDegree])
                    {
                        case ACCIDENTAL.FLAT:                
                            $natural = natural(x, stavePosition);
                            this.$staveContainer.append($natural);
                            x++;

                            $sharp = sharp(x, stavePosition);
                            this.$staveContainer.append($sharp);
                            x++;
                
                        break;

                        case ACCIDENTAL.NATURAL:
                            $sharp = sharp(x, stavePosition);
                            this.$staveContainer.append($sharp);
                            x++;                        
                        break;

                        case ACCIDENTAL.SHARP:
                            
                            // Nothing to see here.
                        break;
                    }
                break;
            }            
            
            // We're going to need a note head.
            var $head = notehead(x, stavePosition, hoverNotehead);
            this.$staveContainer.append($head);
                        
            // Do the line for the actual note.
            $line = line(x, stavePosition, contextualDegree.diatonicRelative);
            this.$staveContainer.append($line);
                        
            x++;
            
            // And finally...
            // Set the accidental state for that pitch class for next time round.
            accidentalState[contextualDegree.diatonicDegree] = contextualDegree.diatonicAccidental;
        
        
            var self = this;
            // And more finally...
            if (isLastPosition && !hoverNotehead)
            {
                x++;
                var $backspace = backspace(x, function(){self.backspace();});
                this.$staveContainer.append($backspace);                
            }
            
            if (isLastPosition && (display_pitches.length >= MIN_NOTES))
            {
                x++;
                var pitches_copy = self.pitches.slice();
                var $submit = submitButton(x, function(){self.musick.submitMelodyQuery(pitches_copy);});
                this.$staveContainer.append($submit);
            }
        }        
    };
    
    this.backspace = function()
    {
        this.pitches.pop();
        this.render();
    };
}


function Musick($keyboardContainer, $staveContainer, $melodySearchForm)
{
    this.keyboard = new Keyboard($keyboardContainer, this);
    this.stave = new Stave($staveContainer, this);
    this.$melodySearchForm = $melodySearchForm;
    
    this.stave.render();
    
    this.keyboard_click = function(pitch)
    {
        this.stave.setPitch(pitch);
        this.stave.render();
    };
    
    this.keyboard_hover = function(pitch)
    {
        this.stave.setHoverPitch(pitch);
        this.stave.render();
    };
    
    this.keyboard_unhover = function(pitch)
    {
        this.stave.setHoverPitch(null);
        this.stave.render();
    };
    
    this.submitMelodyQuery = function(melodySequence)
    {
        var $melody, $melodySearchForm;
        
        $melodySearchForm = $("#melody-search-form");
        $melody = $("#melody-search-form #id_melody");
        $melody.val(melodySequence);
        $melodySearchForm.submit();
    };
}

function display_homepage_keyboard()
{
    var $keyboardContainer = $("#keyboard");
    var $staveContainer = $("#stave");
    var $melodySearchForm = $("#melody-search-form");

    var musick = new Musick($keyboardContainer, $staveContainer, $melodySearchForm);
}
