package jp.crestmuse.cmx.misc;

import jp.crestmuse.cmx.filewrappers.*;
import jp.crestmuse.cmx.filewrappers.MusicXMLWrapper.Measure;
import jp.crestmuse.cmx.filewrappers.MusicXMLWrapper.MusicData;
import jp.crestmuse.cmx.filewrappers.MusicXMLWrapper.Part;
import jp.crestmuse.cmx.filewrappers.SCCXMLWrapper.Note;
import jp.crestmuse.cmx.handlers.*;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.*;
  
import org.xml.sax.SAXException;

import java.io.IOException;
import java.util.*;

public class PerformanceMatcher2 {

  private static final int BASE_TEMPO = 120;

  public static DeviationInstanceWrapper extractDeviation
    (MusicXMLWrapper score, MIDIXMLWrapper pfm) 
    throws ParserConfigurationException, SAXException, IOException,
    TransformerException {
    DeviationInstanceWrapper dev = 
      DeviationInstanceWrapper.createDeviationInstanceFor(score);
    DeviationDataSet dds = dev.createDeviationDataSet();
    SCCXMLWrapper scoreSCC = score.makeDeadpanSCCXML(pfm.ticksPerBeat());
    SCCXMLWrapper pfmSCC = pfm.toSCCXML();
    SCCXMLWrapper sortedScore = scoreSCC.getSortedSCCXML(50);
    SCCXMLWrapper sortedPfm = pfmSCC.getSortedSCCXML(100);
    int[] path = getPath(dtw(sortedScore, sortedPfm, 0));
    ArrayList<Double> beatToTempo = getTempoList(sortedScore, sortedPfm, path);
    setTempoDev(dds, beatToTempo, BASE_TEMPO, score);
    setDeviation(dds, sortedScore, sortedPfm, path, beatToTempo);
    dds.addElementsToWrapper();
    return dev;
  }

  
/*
  // moved to SCCXMLWrapper
  public static SCCXMLWrapper sortSCC(SCCXMLWrapper scc, int x) throws SAXException, ParserConfigurationException, TransformerException, IOException{
    SCCXMLWrapper result = (SCCXMLWrapper)CMXFileWrapper.createDocument(SCCXMLWrapper.TOP_TAG);
    
    result.setDivision(scc.getDivision());
    
    SCCXMLWrapper.Part[] parts = scc.getPartList();
    for(SCCXMLWrapper.Part part : parts){
      result.newPart(part.serial(), part.channel(), part.prognum(), part.volume());
      Object[] notes = scc.getPartList()[part.prognum()].getSortedNoteSet(x).toArray();
      for(Object obj : notes){
        SCCXMLWrapper.Note note = (SCCXMLWrapper.Note)obj;
        result.addNoteElement(note.onset(), note.offset(), note.notenum(), note.velocity(), note.getMusicXMLWrapperNote());
      }
      result.endPart();
    }
    
    result.finalizeDocument();
    return result;
  }
*/
  

  // 第２案
  private static int[] getPath(Note[] score, Note[] pfm, int r){
    int[] path = new int[score.length];
    int dev = 0;
    for(int i=0; i<score.length; i++){
      int j=i;
      while(true){
        try {
          if(Math.abs(score[j+1].onset()-score[j].onset()) > r) break;
        } catch (ArrayIndexOutOfBoundsException e) {break;}
        j++;
      }
      
      int k=i+dev;
      while(true){
        try {
          if(Math.abs(pfm[k+1].onset()-pfm[k].onset()) > r) break;
        } catch (ArrayIndexOutOfBoundsException e) {break;}
        k++;
      }
      
      if((j-i) == k-(i+dev)) for(int l=i; l<=j; l++) path[l] = l+dev;
      else if((j-i) < k-(i+dev)){
        for(int l=i; l<=j; l++) path[l] = l+dev;
        dev += (k-(i+dev)) - (j-i);
      }else{
        //int min = (j-i) - (k-(i+dev));
        System.out.println(i+"\t"+j+"\t"+k+"\t"+dev);
      }
      
      i=j;
    }
    return path;
  }
  
  private static int[] getPath(DTWMatrix matrix) {
    int I = matrix.nrows;
    int J = matrix.ncols;
    int[] path = new int[I];
    int i = I - 1, j = J - 1;
    do {
      path[i] = j;
      DTWMatrix.DTWMatrixElement e = matrix.get(i, j);
      i = e.targetX;
      j = e.targetY;
    } while (i >= 0 || j >= 0);
    return path;
  }
  
  private static DTWMatrix
  dtw(SCCXMLWrapper score, SCCXMLWrapper pfm, int r) throws TransformerException {
    int I = score.getPartList()[0].getNoteList().length;
    int J = pfm.getPartList()[0].getNoteList().length;
    if (r <= 0)
      r = J;
    DTWMatrix matrix = new DTWMatrix(I, J);
    matrix.set(-1, -1, 0, -1, -1);
    for (int i = 0; i < I; i++) {
      //System.err.print(".");
      for (int j = Math.max(0, i-r) ; j <= Math.min(i+r, J-1); j++) {
        Note e1 = score.getPartList()[0].getNoteList()[i];
        Note e2 = pfm.getPartList()[0].getNoteList()[j];
        //int d = Math.abs(e1.notenum() - e2.notenum());
        int d = dist(e1, e2);
        int c1 = matrix.getValue(i-1, j) + d;
        int c2 = matrix.getValue(i-1, j-1) + 2 * d;
        int c3 = matrix.getValue(i, j-1) + d;
        int c_min = Math.min(c2, Math.min(c1, c3));
        if (c_min == c2)
          matrix.set(i, j, c_min, i-1, j-1);
        else if (c_min == c3)
          matrix.set(i, j, c_min, i, j-1);
        else
          matrix.set(i, j, c_min, i-1, j);
      }
    }
    return matrix;
  }
  
  private static int dist(Note e1, Note e2){
    if(e1.notenum() == e2.notenum()) return 0;
    return Math.abs(e1.onset() - e2.onset());
  }

  private static class DTWMatrix {
    private HashMap<IntPair,DTWMatrixElement> values;
    private int nrows, ncols;
    private final DTWMatrixElement DEFAULT_MATRIX_ELEMENT = 
      new DTWMatrixElement(Integer.MAX_VALUE / 2, -1, -1);
    private DTWMatrix(int nrows, int ncols) {
      this.nrows = nrows;
      this.ncols = ncols;
      values = new HashMap<IntPair,DTWMatrixElement>(Math.max(nrows,ncols));
    }
    private void set(int i, int j, int value, int targetX, int targetY) {
      values.put(new IntPair(i, j), 
                 new DTWMatrixElement(value, targetX, targetY));
    }
    private DTWMatrixElement get(int i, int j) {
      if (i >= nrows || j >= ncols || i < -1 || j < -1)
        throw new ArrayIndexOutOfBoundsException();
      if (values.containsKey(new IntPair(i, j)))
        return values.get(new IntPair(i, j));
      else
        return DEFAULT_MATRIX_ELEMENT;
    }
    private int getValue(int i, int j) {
      return get(i, j).value;
    }
    private class IntPair {
      private int i, j;
      private IntPair(int i, int j) {
        this.i = i;
        this.j = j;
      }
      public boolean equals(Object o) {
        IntPair another = (IntPair)o;
        return (i == another.i) && (j == another.j);
      }
      public int hashCode() {
        return i * nrows + j;
      }
    }
    private class DTWMatrixElement {
      private int value, targetX, targetY;
      private DTWMatrixElement(int value, int targetX, int targetY){
        this.value = value;
        this.targetX = targetX;
        this.targetY = targetY;
      }
    }
  }

  private static double calcInitialSilence(SCCXMLWrapper score, 
                                          SCCXMLWrapper pfm) 
                                          throws TransformerException {
    int div1 = score.getDivision();
    int div2 = pfm.getDivision();
    SCCXMLWrapper.Part[] scoreParts = score.getPartList();
    SCCXMLWrapper.Part[] pfmParts = pfm.getPartList();
    int nParts = Math.min(scoreParts.length, pfmParts.length);
    for (int i = 0; i < nParts; i++) {
      int t1 = scoreParts[i].getNoteList()[0].onset();
      if (t1 == 0) {
        int t2 = pfmParts[i].getNoteList()[0].onset();
        double sil;
        if (div1 == div2) 
          sil = (double)((t2 - t1) * 60)/ (double)(div1 * BASE_TEMPO);
        else
          sil = (double)((t2*div2-t1*div1)*60) / (double)(div1*div2*BASE_TEMPO);
        return sil;
      }
    }
    return 0;
  }


  
  private static void setDeviation(DeviationDataSet dds,
      SCCXMLWrapper score, SCCXMLWrapper pfm,
      int[] path, ArrayList<Double> beatToTempo) throws TransformerException{
    
    Note[] scoreNotes = score.getPartList()[0].getNoteList();
    Note[] pfmNotes = pfm.getPartList()[0].getNoteList();

    System.out.println(scoreNotes[0].onset());
    System.out.println(pfmNotes[0].onset());
    
    double initsil = calcInitialSilence(score, pfm);
    dds.setInitialSilence(initsil);
    int initticks = (int)(initsil * BASE_TEMPO * pfm.getDivision() / 60);

    double timeStamp = 0;
    ArrayList<Double> beatToTick = new ArrayList<Double>();
    beatToTick.add(0.);
    for(int i=0; i<beatToTempo.size(); i++){
      timeStamp += score.getDivision() * 120. / beatToTempo.get(i);
      beatToTick.add(timeStamp);
    }
    
    for(int i=0; i<scoreNotes.length; i++){
      Note s = scoreNotes[i];
      Note p = pfmNotes[path[i]];
      int beat = s.onset() / score.getDivision();
      
      try {
        double attack = (p.onset() - initticks - (beatToTick.get(beat) + (s.onset() % score.getDivision()) * 120. / beatToTempo.get(beat))) / (double)score.getDivision() /120. * beatToTempo.get(beat);
        double release = (p.offset() - initticks - (beatToTick.get((s.offset()-1)/score.getDivision()) + (s.offset() % score.getDivision()) * 120. / beatToTempo.get((s.offset()-1)/score.getDivision()))) / (double)score.getDivision() / 120. * beatToTempo.get((s.offset()-1)/score.getDivision());
        double dynamics = p.velocity() / (double)s.velocity();
        
        dds.addNoteDeviation(s.getMusicXMLWrapperNote(), attack, release, dynamics, dynamics);
      } catch (IndexOutOfBoundsException e) { }
    }
  }
  
  private static ArrayList<Double> getTempoList(SCCXMLWrapper score,
      SCCXMLWrapper pfm, int[] path) throws TransformerException{
    
    ArrayList<Double> align = new ArrayList<Double>();
    Note[] scoreNotes = score.getPartList()[0].getNoteList();
    Note[] pfmNotes = pfm.getPartList()[0].getNoteList();
    
    int k = 0;
    for(int i=0; i<scoreNotes.length; i++){
      if(scoreNotes[i].onset() == k){
        int j, total=0;
        for(j=i; ; j++){
          total += pfmNotes[path[j]].onset();
          try {
            if(scoreNotes[j+1].onset() != k) break;
          } catch (ArrayIndexOutOfBoundsException e) {
            break;
          }
        }
        align.add(total/(j-i+1.)/pfm.getDivision()*0.5);
        
        /*int[] list = {121,122,185,186,323,337};
        for(int d:list){
          if(align.size() == d+1){
            System.out.println(total+"\t"+i+"\t"+j);
          }
        }*/
        
        i = j;
        k += score.getDivision();
      }else if(scoreNotes[i].onset() > k){
        align.add(-1.);
        k += score.getDivision();
        i--;
      }
    }
    
    //for(int d=461; d<=463; d++)
    //  System.out.println(d+"\t" + scoreNotes[d].onset()+"\t" + path[d]+"\t" + pfmNotes[path[d]].onset());
    
    for(int i=0; i<align.size(); i++){
      if(align.get(i) == -1){
        int j=0;
        while(align.get(i+j) == -1) j++;
        double span = (align.get(i+j) - align.get(i-1)) / (j+1);
        for(int l=i; l<i+j; l++){
          align.set(l, align.get(l-1)+span);
        }
      }
    }
    
    ArrayList<Double> beatToTempo = new ArrayList<Double>();
    for(int i=0; i<align.size()-1; i++){
      beatToTempo.add(60/(align.get(i+1)-align.get(i)));
    }
    //for(int i=0; i<beatToTempo.size(); i++) if(beatToTempo.get(i).isInfinite())System.out.println(i);
    return beatToTempo;
  }
  
  private static void setTempoDev(DeviationDataSet dds,
      ArrayList<Double> tempoList, int baseTempo, MusicXMLWrapper score){
    
    double total=0;
    for(double d : tempoList) total += d;
    double average = total / tempoList.size();
    double _baseTempo = average;

    // 元のMusicXMLから開始小節の番号とビートタイプを得る
    Part[] parts = score.getPartList();
    Measure[] measures = parts[0].getMeasureList();
/*
    int beats = 0;
    int beatType = 0;
    for(MusicData md : measures[0].getMusicDataList()){
      if(md instanceof MusicXMLWrapper.Attributes){
        MusicXMLWrapper.Attributes att = (MusicXMLWrapper.Attributes)md;
        beats = att.beats();
        beatType = att.beatType();
        break;
      }
    }
*/
    int headMeasure = measures[0].number();
 
    // 反映する
    dds.addNonPartwiseControl(headMeasure, 1, "tempo", _baseTempo);

    Iterator<Double> it = tempoList.iterator();
    try {
      for (Measure measure : measures) {
        int length = measure.duration(1);
        for (int i = 1; i <= length; i++) 
          dds.addNonPartwiseControl(measure.number(), i, "tempo-deviation", 
                                    it.next() / average);
      }
    } catch (NoSuchElementException e) { }

/*
    int index = 0;
    for(int i=0; i<measures.length; i++){
      for(int j=1; j<=beats*4/beatType; j++){
        try {
          dds.addNonPartwiseControl(i+headMeasure, j, "tempo-deviation", tempoList.get(index++)/avarage);
        } catch (IndexOutOfBoundsException e) {}
      }
    }
*/
  
  }
  
  /*private static 
    TimeFreqRepresentation<SCCXMLWrapper.Note> 
    getTimeFreqRepresentation(SCCXMLWrapper scc) 
    throws TransformerException {
    final int division = scc.getDivision();
    final TimeFreqRepresentation<SCCXMLWrapper.Note> tfr 
      = new TimeFreqRepresentation<SCCXMLWrapper.Note>(N_BANDS);
    scc.processNotes(new SCCHandler() {
        public void beginHeader(SCCXMLWrapper w) { }
        public void endHeader(SCCXMLWrapper w) { }
        public void beginPart(SCCXMLWrapper.Part part, SCCXMLWrapper w) {}
        public void endPart(SCCXMLWrapper.Part part, SCCXMLWrapper w) {}
        public void processHeaderElement(int timestamp, String name, 
                                         String content, SCCXMLWrapper w) {}
        public void processNote(SCCXMLWrapper.Note note, SCCXMLWrapper w) {
          int onset = note.onset();
          int offset = note.offset();
          int notenum = note.notenum();
          int length;
          while ((length = tfr.length()) < offset)
            tfr.addTime(length * division);
          tfr.set(onset, notenum, note);
          for (int n = onset; n < offset; n++)
            tfr.set(n, notenum, (byte)127);
        }
      });
    return tfr;
  }*/

/*
  public static void main(String[] args) {
    try {
      int range = Integer.parseInt(args[3]);
      MIDIXMLWrapper midixml = MIDIXMLWrapper.readSMF(args[1]);
      SCCXMLWrapper scc1 = midixml.toSCCXML();
      scc1 = sortSCC(scc1, range);
      //TimeFreqRepresentation tfr1 = getTimeFreqRepresentation(scc1);
      MusicXMLWrapper musicxml = (MusicXMLWrapper)CMXFileWrapper.readfile(args[0]);
      SCCXMLWrapper scc0 = musicxml.makeDeadpanSCCXML(scc1.getDivision());
      scc0 = sortSCC(scc0, range);
      //TimeFreqRepresentation tfr0 = getTimeFreqRepresentation(scc0);
      //System.out.println(tfr0.length());
      //System.out.println(tfr1.length());
      DTWMatrix matrix = dtw(scc0, scc1, Integer.parseInt(args[2]));
      int[] path = getPath(matrix);
      for (int n : path)
        System.out.print(n + " ");
    } catch (Exception e) {
      e.printStackTrace();
      System.exit(1);
    }
  }
*/

}
  