package ac.essex.ooechs.music.imaging;

import ac.essex.ooechs.imaging.commons.PixelLoader;
import ac.essex.ooechs.imaging.commons.Pixel;
import ac.essex.ooechs.imaging.commons.util.LineEquation;
import ac.essex.ooechs.imaging.commons.apps.shapes.Grouper;
import ac.essex.ooechs.imaging.commons.apps.shapes.SegmentedShape;
import ac.essex.ooechs.imaging.commons.apps.shapes.ExtraShapeData;
import ac.essex.ooechs.music.imaging.evolved.LineSegmenter;
import ac.essex.ooechs.music.imaging.evolved.NoteSegmenter;
import ac.essex.ooechs.music.util.Key;
import ac.essex.ooechs.music.util.Staff;

import java.util.Vector;

/**
 * <p/>
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version,
 * provided that any use properly credits the author.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details at http://www.gnu.org
 * </p>
 *
 * @author Olly Oechsle, University of Essex, Date: 05-Jul-2007
 * @version 1.0
 */
public class MusicParser {

    public Vector<Staff> staffs;
    public LineEquation cursor;
    public Vector<Pixel> notes;
    public Vector<Pixel> points;
    public Vector<LineEquation> lines;

    public int startX, endX;

    public MusicParser(PixelLoader image) {

        Key.reset();

        Key.setFlat("a");
        Key.setFlat("b");
        Key.setFlat("d");
        Key.setFlat("e");

        getLines(image);
        getNotes(image);

        Vector<Pixel> removeNotes = new Vector<Pixel>();

        // remove all lines to the left of the x-min
        for (int i = 0; i < notes.size(); i++) {
            Pixel note = notes.elementAt(i);
            if (note.x < startX + staffs.elementAt(0).getHeight()) {
                removeNotes.add(note);
            }
        }

        notes.removeAll(removeNotes);        

    }

    private void getNotes(PixelLoader image) {
        int[][] pixels = new NoteSegmenter().segment(image);

        notes = new Vector<Pixel>();

        Grouper g = new Grouper();
        Vector<SegmentedShape> shapes = g.getShapes(pixels);

        for (int i = 0; i < shapes.size(); i++) {
            SegmentedShape shape = shapes.elementAt(i);

            try {

                if (shape.getMass() > 1000) continue;

                ExtraShapeData esd = new ExtraShapeData(shape);

                if (shape.getMass() > 2 && esd.getRoundness() < 0.55) {
                    //Pixel cog = esd.getCentreOfGravity();
                    int width = shape.maxX - shape.minX;
                    int height = shape.maxY - shape.minY;
                    notes.add(new Pixel(shape.minX + (width / 2), shape.minY + (height / 2)));
                } else {
                    System.out.println("esd.getRoundness():" + esd.getRoundness());
                }

            } catch (Exception e) {
                // never mind
                System.out.println("ARG");
            }

        }

    }

    private void getLines(PixelLoader image) {
        points = new Vector<Pixel>();

        // Segment the image to find the clefs
        int[][] pixels = new LineSegmenter().segment(image);

        // Group the clefs into shapes
        Grouper g = new Grouper();
        Vector<SegmentedShape> shapes = g.getShapes(pixels);

        double lowestError = Integer.MAX_VALUE;
        double highestC = Integer.MIN_VALUE;
        double lowestC = Integer.MAX_VALUE;

        // Search for the line which fits the image line best
        LineEquation best = null;

        // for each shape find the bottom left coordinate and the top right coordinate.
        for (int i = 0; i < shapes.size(); i++) {
            SegmentedShape shape = shapes.elementAt(i);

            if (shape.getMass() > 30) {

                try {

                    // calculate the equation of a line
                    LineEquation line = new LineEquation(shape.minX, shape.maxY, shape.maxX, shape.minY);

                    points.add(new Pixel(shape.minX, shape.maxY));
                    points.add(new Pixel(shape.maxX, shape.minY));

                    double error = getHorizontalLineError(line, image);
                    if (error < lowestError) {
                        best = line;
                        lowestError = error;
                    }

                    if (line.getC() < lowestC) lowestC = line.getC();
                    if (line.getC() > highestC) highestC = line.getC();

                } catch (RuntimeException e) {
                    // if the line isn't really a line
                }

            }

        }

        if (best == null) {
            System.err.println("Could not find good enough line");
        }

        staffs = new Vector<Staff>();

        // now move up and down and find the group of five clefs that makes up the stave.
        // find error minima that signify the positions of the clefs
        LineEquation[] stave = new LineEquation[5];
        staffs.add(new Staff(stave, Staff.BASS));

        int currentIndex = 0;
        double currentBest = Double.MAX_VALUE;
        boolean added = false;

        // move up from bottom of the image
        for (int c = image.getHeight(); c > 0; c--) {
        //for (int c = 0; c < image.getHeight(); c++) {
            try {
                LineEquation next = new LineEquation(best.getM(), c);
                double error = getHorizontalLineError(next, image);
                if (error < 0.5 && error < currentBest) {

                    if (currentIndex == 5) {
                        // finished one stave
                        stave = new LineEquation[5];
                        staffs.add(new Staff(stave, Staff.TREBLE));
                        currentIndex = 0;
                    }

                    stave[currentIndex] = next;
                    currentBest = error;
                    added = true;
                }
                if (error > 0.5 && added) {
                    currentIndex++;
                    currentBest = 1.0;
                    added = false;
                }
            } catch (ArrayIndexOutOfBoundsException e) {
                // do nothing, expected.
            }
        }

        if (currentIndex < 4)  {
            System.err.println("Error: Could not find enough clefs for full stave!");
            staffs.remove(staffs.lastElement());
        }

        // find the perpendicular line
        cursor = staffs.elementAt(0).clefs[0].getPerpendicular();

        // find the x bounds
        for (int x = 0; x < 100; x++) {
            try {
                double e = getVerticalLineError(cursor, x, image);
                if (e < 0.9) {
                    startX = x;
                    break;
                }
            } catch (ArrayIndexOutOfBoundsException e) {
                // do nothing
            }
        }

        for (int x = image.getWidth(); x > (image.getWidth() - 100); x--) {
            try {
                double e = getVerticalLineError(cursor, x, image);
                if (e < 0.9) {
                    endX = x;
                    break;
                }
            } catch (ArrayIndexOutOfBoundsException e) {
                // do nothing
            }
        }

        // loop through the staffs and set their types accurately
        // todo: for now this is inaccurate: assuming treble followed by bass.
/*        boolean bass = false;
        for (int i = 0; i < staffs.size(); i++) {
            Staff staff =  staffs.elementAt(i);
            staff.type = bass? Staff.BASS : Staff.TREBLE;
            bass = !bass;
        }*/

        lines = new Vector<LineEquation>();

        lines.add(new LineEquation(0, 0));

        // Now count how many lines there are
        Staff lastStaff = null;
        for (int i = 0; i < staffs.size(); i++) {
            Staff staff = staffs.elementAt(i);
            if (lastStaff != null && lastStaff.type == Staff.BASS && staff.type == Staff.TREBLE) {
                LineEquation l1 = lastStaff.clefs[0];
                LineEquation l2 = staff.clefs[4];
                double c = (l1.getC() + l2.getC()) / 2;
                lines.add(new LineEquation(l1.getM(), c));
            }
            lastStaff = staff;
        }

        if (staffs.size() == 1) {
            staffs.elementAt(0).type = Staff.TREBLE;
        }




        lines.add(new LineEquation(0, image.getHeight() - 1));

    }

    private double getVerticalLineError(LineEquation line, int xOffset, PixelLoader image) {
        double myError = 0;
        int total = 0;

        for (int y = 0; y < image.getHeight(); y += 5) {
            myError += image.getGreyValue((int) line.getX(y) + xOffset, y);
            total += 255;
        }

        return myError / total;
    }

    private double getHorizontalLineError(LineEquation line, PixelLoader image) {
        double myError = 0;
        int total = 0;

        for (int x = 0; x < image.getWidth(); x += 5) {
            myError += image.getGreyValue(x, (int) line.getY(x));
            total += 255;
        }

        return myError / total;
    }

}
