#include "importmidi_voice.h"

#include "importmidi_tuplet.h"
#include "importmidi_inner.h"
#include "importmidi_chord.h"
#include "importmidi_meter.h"
#include "importmidi_operations.h"
#include "libmscore/sig.h"
#include "libmscore/mscore.h"
#include "mscore/preferences.h"
#include "libmscore/durationtype.h"


namespace Ms {
namespace MidiVoice {

// no more than VOICES

int toIntVoiceCount(MidiOperations::VoiceCount value)
      {
      switch (value) {
            case MidiOperations::VoiceCount::V_1:
                  return 1;
            case MidiOperations::VoiceCount::V_2:
                  return 2;
            case MidiOperations::VoiceCount::V_3:
                  return 3;
            case MidiOperations::VoiceCount::V_4:
                  return 4;
            }
      return VOICES;
      }

int voiceLimit()
      {
      const auto &opers = preferences.midiImportOperations.data()->trackOpers;
      const int currentTrack = preferences.midiImportOperations.currentTrack();
      const int allowedVoiceCount = toIntVoiceCount(opers.maxVoiceCount.value(currentTrack));

      Q_ASSERT_X(allowedVoiceCount <= VOICES,
                 "MidiVoice::voiceLimit",
                 "Allowed voice count exceeds MuseScore voice limit");

      return allowedVoiceCount;
      }


#ifdef QT_DEBUG

bool areNotesSortedByOffTimeInAscOrder(
            const QList<MidiNote>& notes,
            const std::vector<int> &groupOfIndexes)
      {
      for (int i = 0; i != (int)groupOfIndexes.size() - 1; ++i) {
            if (notes[groupOfIndexes[i]].offTime > notes[groupOfIndexes[i + 1]].offTime)
                  return false;
            }
      return true;
      }

bool areNotesSortedByOffTimeInAscOrder(const QList<MidiNote>& notes)
      {
      for (int i = 0; i != (int)notes.size() - 1; ++i) {
            if (notes[i].offTime > notes[i].offTime)
                  return false;
            }
      return true;
      }

bool doesTupletAlreadyExist(
            const ReducedFraction &tupletOnTime,
            int voice,
            const std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets)
      {
      const auto range = tuplets.equal_range(tupletOnTime);
      bool found = false;
      for (auto it = range.first; it != range.second; ++it) {
            if (it->second.voice == voice) {
                  found = true;
                  break;
                  }
            }
      return found;
      }

bool areVoicesSame(const std::multimap<ReducedFraction, MidiChord> &chords)
      {
      for (const auto &chord: chords) {
            const MidiChord &c = chord.second;
            if (c.isInTuplet && c.tuplet->second.voice != c.voice)
                  return false;
            for (const auto &note: c.notes) {
                  if (note.isInTuplet && note.tuplet->second.voice != c.voice)
                        return false;
                  }
            }
      return true;
      }

#endif


bool allNotesHaveEqualLength(const QList<MidiNote> &notes)
      {
      const auto &offTime = notes[0].offTime;
      for (int i = 1; i != notes.size(); ++i) {
            if (notes[i].offTime != offTime)
                  return false;
            }
      return true;
      }

int findDurationCountInGroup(
            const ReducedFraction &chordOnTime,
            const QList<MidiNote> &notes,
            int voice,
            const std::vector<int> &groupOfIndexes,
            const TimeSigMap *sigmap,
            const std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets)
      {
      Q_ASSERT_X(areNotesSortedByOffTimeInAscOrder(notes, groupOfIndexes),
                 "MidiVoice::findDurationCountInGroup",
                 "Notes are not sorted by off time in ascending order");

      const auto &opers = preferences.midiImportOperations.data()->trackOpers;
      const int currentTrack = preferences.midiImportOperations.currentTrack();
      const bool useDots = opers.useDots.value(currentTrack);

      int count = 0;
      auto onTime = chordOnTime;
      auto onTimeBarStart = MidiBar::findBarStart(onTime, sigmap);
      auto onTimeBarFraction = ReducedFraction(
                        sigmap->timesig(onTimeBarStart.ticks()).timesig());

      for (int i: groupOfIndexes) {
            const auto &offTime = notes[i].offTime;
            if (offTime == onTime)
                  continue;
            const auto offTimeBarStart = MidiBar::findBarStart(offTime, sigmap);

            if (offTimeBarStart != onTimeBarStart) {
                  const auto offTimeBarFraction = ReducedFraction(
                              sigmap->timesig(offTimeBarStart.ticks()).timesig());

                  const auto tupletsForDuration = MidiTuplet::findTupletsInBarForDuration(
                              voice, onTimeBarStart, onTime, offTimeBarStart - onTime, tuplets);

                              // additional durations on measure boundary
                  const auto durations = Meter::toDurationList(
                              onTime - onTimeBarStart, offTimeBarStart - onTimeBarStart,
                              offTimeBarFraction, tupletsForDuration, Meter::DurationType::NOTE,
                              useDots, false);

                  count += MidiDuration::durationCount(durations);

                  onTime = offTimeBarStart;
                  onTimeBarStart = offTimeBarStart;
                  onTimeBarFraction = offTimeBarFraction;
                  }

            const auto tupletsForDuration = MidiTuplet::findTupletsInBarForDuration(
                              voice, onTimeBarStart, onTime, offTime - onTime, tuplets);

            const auto durations = Meter::toDurationList(
                              onTime - onTimeBarStart, offTime - onTimeBarStart, onTimeBarFraction,
                              tupletsForDuration, Meter::DurationType::NOTE, useDots, false);

            count += MidiDuration::durationCount(durations);

            onTime = offTime;
            }
      return count;
      }

// count of resulting durations in music notation

int findDurationCount(
            const QList<MidiNote> &notes,
            int voice,
            int splitPoint,
            const ReducedFraction &chordOnTime,
            const TimeSigMap *sigmap,
            const std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets)
      {
      std::vector<int> lowGroup;
      std::vector<int> highGroup;

      for (int i = 0; i != splitPoint; ++i)
            lowGroup.push_back(i);
      for (int i = splitPoint; i != notes.size(); ++i)
            highGroup.push_back(i);

      std::sort(lowGroup.begin(), lowGroup.end(),
                [&](int i1, int i2) { return notes[i1].offTime < notes[i2].offTime; });
      std::sort(highGroup.begin(), highGroup.end(),
                [&](int i1, int i2) { return notes[i1].offTime < notes[i2].offTime; });

      return findDurationCountInGroup(chordOnTime, notes, voice, lowGroup, sigmap, tuplets)
             + findDurationCountInGroup(chordOnTime, notes, voice, highGroup, sigmap, tuplets);
      }

int findOptimalSplitPoint(
            const std::multimap<ReducedFraction, MidiChord>::iterator &chordIt,
            const TimeSigMap *sigmap,
            const std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets,
            const std::multimap<ReducedFraction, MidiChord> &chords)
      {
      const auto &notes = chordIt->second.notes;

      Q_ASSERT_X(!notes.isEmpty(),
                 "MidiVoice::findOptimalSplitPoint", "Notes are empty");
      Q_ASSERT_X(areNotesSortedByOffTimeInAscOrder(notes),
                 "MidiVoice::findOptimalSplitPoint",
                 "Notes are not sorted by length in ascending order");

      int optSplit = -1;

      if (!allNotesHaveEqualLength(notes)) {
            int minNoteCount = std::numeric_limits<int>::max();

            for (int splitPoint = 1; splitPoint != notes.size(); ++splitPoint) {
                              // optimization: don't split notes with equal durations
                  if (notes[splitPoint - 1].offTime == notes[splitPoint].offTime)
                        continue;
                  int noteCount = findDurationCount(notes, chordIt->second.voice, splitPoint,
                                                    chordIt->first, sigmap, tuplets);
                  if (noteCount < minNoteCount) {
                        minNoteCount = noteCount;
                        optSplit = splitPoint;
                        }
                  }

            Q_ASSERT_X(optSplit != -1,
                       "MidiVoice::findOptimalSplitPoint", "Optimal split point was not defined");
            }
      else {
            const auto offTime = notes.front().offTime;
            for (auto it = std::next(chordIt); it != chords.end(); ++it) {
                  if (it->first >= offTime)
                        break;
                  if (it->second.voice != chordIt->second.voice)
                        continue;
                  if (it->first < offTime) {
                        optSplit = 0;
                        break;
                        }
                  }
            }

      return optSplit;
      }

// which part of chord notes, sorted by length - low note indexes or high note indexes
// - should be moved to another voice

enum class MovedVoiceGroup {
      LOW,
      HIGH
      };

struct VoiceSplit {
      MovedVoiceGroup group;
      int voice = -1;
      };

std::multimap<ReducedFraction,
                  std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator>::const_iterator
findInsertedTuplet(const ReducedFraction &onTime,
           int voice,
           const std::multimap<ReducedFraction,
                 std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator> &insertedTuplets)
      {
      const auto range = insertedTuplets.equal_range(onTime);
      for (auto it = range.first; it != range.second; ++it) {
            if (it->second->second.voice == voice)
                  return it;
            }
      return insertedTuplets.end();
      }

// if new chord intersected with tuplet that already was inserted
// due to some previous chord separation - then it is not an intersection:
// the new chord belongs to this tuplet

bool hasIntersectionWithTuplets(
            int voice,
            const ReducedFraction &onTime,
            const ReducedFraction &offTime,
            const std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets,
            const std::multimap<ReducedFraction,
                  std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator> &insertedTuplets,
            const ReducedFraction &tupletOnTime)
      {
      const auto foundTuplets = MidiTuplet::findTupletsForTimeRange(
                                          voice, onTime, offTime - onTime, tuplets, true);
      for (const auto tupletIt: foundTuplets) {
            const auto ins = findInsertedTuplet(tupletIt->first, voice, insertedTuplets);
            const bool belongsToInserted = (ins != insertedTuplets.end()
                                            && ins->first == tupletOnTime);
            if (!belongsToInserted)
                  return true;
            }

      return false;
      }

void addGroupSplits(
            std::vector<VoiceSplit> &splits,
            const ReducedFraction &maxChordLength,
            const std::multimap<ReducedFraction, MidiChord> &chords,
            const std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets,
            const std::multimap<ReducedFraction,
                  std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator> &insertedTuplets,
            const ReducedFraction &tupletOnTime,
            const ReducedFraction &onTime,
            const ReducedFraction &groupOffTime,
            int origVoice,
            MovedVoiceGroup groupType,
            int maxOccupiedVoice)
      {
      const int limit = voiceLimit();
      bool splitAdded = false;

      for (int voice = 0; voice != limit; ++voice) {
            if (voice == origVoice)
                  continue;
            if (voice > maxOccupiedVoice && splitAdded)
                  break;
            if (hasIntersectionWithTuplets(voice, onTime, groupOffTime,
                                           tuplets, insertedTuplets, tupletOnTime))
                  continue;
            const auto foundChords = MChord::findChordsForTimeRange(
                                       voice, onTime, groupOffTime, chords, maxChordLength);
            if (!foundChords.empty())
                  continue;

            splitAdded = true;
            VoiceSplit split;
            split.group = groupType;
            split.voice = voice;
            splits.push_back(split);
            }
      }

ReducedFraction maximizeOffTime(const MidiNote &note, const ReducedFraction& offTime)
      {
      auto result = offTime;
      if (note.offTime > offTime)
            result = note.offTime;
      if (note.isInTuplet) {
            const auto &tuplet = note.tuplet->second;
            if (tuplet.onTime + tuplet.len > result)
                  result = tuplet.onTime + tuplet.len;
            }
      return result;
      }

std::vector<VoiceSplit> findPossibleVoiceSplits(
            int origVoice,
            const std::multimap<ReducedFraction, MidiChord>::iterator &chordIt,
            int splitPoint,
            const std::multimap<ReducedFraction, MidiChord> &chords,
            const std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets,
            const std::multimap<ReducedFraction,
                  std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator> &insertedTuplets,
            int maxOccupiedVoice)
      {
      std::vector<VoiceSplit> splits;

      ReducedFraction onTime = chordIt->first;
      ReducedFraction lowGroupOffTime(0, 1);
      ReducedFraction highGroupOffTime(0, 1);

      const auto &notes = chordIt->second.notes;
      for (int i = 0; i != splitPoint; ++i)
            lowGroupOffTime = maximizeOffTime(notes[i], lowGroupOffTime);
      for (int i = splitPoint; i != notes.size(); ++i)
            highGroupOffTime = maximizeOffTime(notes[i], highGroupOffTime);

      ReducedFraction tupletOnTime(-1, 1);
      if (chordIt->second.isInTuplet) {
            const auto &tuplet = chordIt->second.tuplet->second;
            tupletOnTime = tuplet.onTime;
            if (tuplet.onTime < onTime)
                  onTime = tuplet.onTime;
            if (tuplet.onTime + tuplet.len > lowGroupOffTime)
                  lowGroupOffTime = tuplet.onTime + tuplet.len;
            if (tuplet.onTime + tuplet.len > highGroupOffTime)
                  highGroupOffTime = tuplet.onTime + tuplet.len;
            }

      const ReducedFraction maxChordLength = MChord::findMaxChordLength(chords);

      if (splitPoint > 0) {
            addGroupSplits(splits, maxChordLength, chords, tuplets, insertedTuplets,
                           tupletOnTime, onTime, lowGroupOffTime, origVoice,
                           MovedVoiceGroup::LOW, maxOccupiedVoice);
            }
      if (splitPoint < notes.size()) {
            addGroupSplits(splits, maxChordLength, chords, tuplets, insertedTuplets,
                           tupletOnTime, onTime, highGroupOffTime, origVoice,
                           MovedVoiceGroup::HIGH, maxOccupiedVoice);
            }

      return splits;
      }

const int MAX_PITCH_DIST = 1000;

int findPrevPitchDist(
            const std::multimap<ReducedFraction, MidiChord>::iterator &chordIt,
            int averagePitch,
            const std::multimap<ReducedFraction, MidiChord> &chords,
            int voice)
      {
      auto it = chordIt;
      while (it != chords.begin()) {
            --it;
            if (it->second.voice == voice) {
                  return qAbs(MChord::chordAveragePitch(it->second.notes) - averagePitch);
                  }
            }
      return MAX_PITCH_DIST;
      }

int findNextPitchDist(
            const std::multimap<ReducedFraction, MidiChord>::iterator &chordIt,
            int averagePitch,
            const std::multimap<ReducedFraction, MidiChord> &chords,
            int voice)
      {
      auto it = (chordIt == chords.end()) ? chordIt : std::next(chordIt);
      while (it != chords.end()) {
            if (it->second.voice == voice) {
                  return qAbs(MChord::chordAveragePitch(it->second.notes) - averagePitch);
                  }
            ++it;
            }
      return MAX_PITCH_DIST;
      }

int findMinPitchDist(
            int averagePitch,
            const int voice,
            const std::multimap<ReducedFraction, MidiChord>::iterator &chordIt,
            const std::multimap<ReducedFraction, MidiChord> &chords)
      {
      const int OCTAVE = 12;
      const int prevPitchDist = findPrevPitchDist(chordIt, averagePitch, chords, voice);
      const int nextPitchDist = findNextPitchDist(chordIt, averagePitch, chords, voice);

      int pitchDist = MAX_PITCH_DIST;

      if (prevPitchDist < nextPitchDist && prevPitchDist <= OCTAVE)
            pitchDist = prevPitchDist;
      else if (nextPitchDist <= prevPitchDist && nextPitchDist <= OCTAVE)
            pitchDist = nextPitchDist;

      return pitchDist;
      }

int findAverageLowPitch(const QList<MidiNote> &notes, int splitPoint)
      {
      int averageLowPitch = 0;
      for (int j = 0; j != splitPoint; ++j)
            averageLowPitch += notes[j].pitch;
      averageLowPitch = qRound(averageLowPitch * 1.0 / splitPoint);

      return averageLowPitch;
      }

int findAverageHighPitch(const QList<MidiNote> &notes, int splitPoint)
      {
      int averageHighPitch = 0;
      for (int j = splitPoint; j != notes.size(); ++j)
            averageHighPitch += notes[j].pitch;
      averageHighPitch = qRound(averageHighPitch * 1.0 / (notes.size() - splitPoint));

      return averageHighPitch;
      }

VoiceSplit findBestSplit(
            const std::multimap<ReducedFraction, MidiChord>::iterator &chordIt,
            const std::multimap<ReducedFraction, MidiChord> &chords,
            const std::vector<VoiceSplit> &possibleSplits,
            int splitPoint)
      {
                  // to minimize <pitch distance, voice>
      std::pair<int, int> minError{std::numeric_limits<int>::max(),
                                   std::numeric_limits<int>::max()};
      int bestSplit = -1;

      for (int i = 0; i != (int)possibleSplits.size(); ++i) {
            const int voice = possibleSplits[i].voice;
            const auto &notes = chordIt->second.notes;
            int totalPitchDist = 0;

            if (splitPoint > 0) {
                  const int averageLowPitch = findAverageLowPitch(notes, splitPoint);
                  const int lowVoice = (possibleSplits[i].group == MovedVoiceGroup::LOW)
                                          ? voice : chordIt->second.voice;
                  totalPitchDist += findMinPitchDist(averageLowPitch, lowVoice, chordIt, chords);
                  }

            if (splitPoint < notes.size()) {
                  const int averageHighPitch = findAverageHighPitch(notes, splitPoint);
                  const int highVoice = (possibleSplits[i].group == MovedVoiceGroup::HIGH)
                                          ? voice : chordIt->second.voice;
                  totalPitchDist += findMinPitchDist(averageHighPitch, highVoice, chordIt, chords);
                  }

            const std::pair<int, int> error{totalPitchDist, voice};

            if (error < minError) {
                  minError = error;
                  bestSplit = i;
                  }
            }

      Q_ASSERT_X(bestSplit != -1, "MidiVoice::findBestSplit", "Best split was not found");

      return possibleSplits[bestSplit];
      }

void insertNewTuplet(
            std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator &tuplet,
            const ReducedFraction &tupletOnTime,
            int newVoice,
            std::multimap<ReducedFraction, MidiChord> &chords,
            std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets,
            std::multimap<ReducedFraction,
                 std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator> &insertedTuplets)
      {
      MidiTuplet::TupletData newTuplet = tuplet->second;
      newTuplet.voice = newVoice;

      Q_ASSERT_X(!doesTupletAlreadyExist(newTuplet.onTime, newVoice, tuplets),
                 "MidiVoice::addOrUpdateTuplet", "Tuplet already exists");

      tuplet = tuplets.insert({tupletOnTime, newTuplet});
      insertedTuplets.insert({tupletOnTime, tuplet});
                  // maybe impossible due to intersection check but check anyway:
                  // if there is a non-tuplet chord with on time = tuplet on time
                  // then add that chord to the new tuplet
      const auto range = chords.equal_range(tupletOnTime);
      for (auto it = range.first; it != range.second; ++it) {
            MidiChord &chord = it->second;
            if (chord.voice == newVoice && !chord.isInTuplet) {
                  chord.isInTuplet = true;
                  chord.tuplet = tuplet;
                  break;
                  }
            }
      }

bool canSplitTuplet(
            const std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator &tuplet,
            int newVoice,
            const ReducedFraction &chordOnTime,
            const QList<MidiNote> &notes,
            const std::multimap<ReducedFraction,
                  std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator> &insertedTuplets,
            const std::multimap<ReducedFraction, MidiChord> &chords,
            const ReducedFraction &maxChordLength)
      {
      const auto tupletOnTime = tuplet->first;
      const auto &t = tuplet->second;
      const auto insertedTuplet = findInsertedTuplet(tupletOnTime, newVoice, insertedTuplets);

      bool needInsertTuplet = false;
      if (insertedTuplet == insertedTuplets.end()
                  && MidiTuplet::hasNonTrivialChord(chordOnTime, notes, t.onTime, t.len)) {
            needInsertTuplet = true;
            }

      const bool needDeleteOldTuplet = MidiTuplet::isTupletUseless(t.voice, tupletOnTime,
                                                                   t.len, maxChordLength, chords);
                  // insert new tuplet only if old tuplet should be deleted
                  // because parallel equal tuplets with different voices aren't pretty
      if (needInsertTuplet && !needDeleteOldTuplet) {
                  // need to amend chord split - need but cannot insert tuplet,
                  // in that case no changes should be done earlier in this function!
            return false;
            }

      return true;
      }

void splitTuplet(
            std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator &tuplet,
            int newVoice,
            const ReducedFraction &chordOnTime,
            const QList<MidiNote> &notes,
            bool &isInTuplet,
            std::multimap<ReducedFraction,
            std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator> &insertedTuplets,
            std::multimap<ReducedFraction, MidiChord> &chords,
            std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets,
            const ReducedFraction &maxChordLength,
            bool allowParallelTuplets,
            bool isThisAChord)
      {
      Q_ASSERT_X(isInTuplet, "MidiVoice::splitTuplet",
                 "Tuplet chord/note is not actually in tuplet");

      const auto oldTuplet = tuplet;
      const auto tupletOnTime = tuplet->first;
      const auto insertedTuplet = findInsertedTuplet(tupletOnTime, newVoice, insertedTuplets);

      bool needInsertTuplet = false;

      if (insertedTuplet == insertedTuplets.end()) {
            const auto &t = tuplet->second;
            if (MidiTuplet::hasNonTrivialChord(chordOnTime, notes, t.onTime, t.len))
                  needInsertTuplet = true;
            else
                  isInTuplet = false;
            }
      else {
            tuplet = insertedTuplet->second;
            }

      const auto &t = oldTuplet->second;
      const bool needDeleteOldTuplet = MidiTuplet::isTupletUseless(
                                          t.voice, t.onTime, t.len, maxChordLength, chords);
                  // insert new tuplet only if old tuplet was erased
                  // because parallel equal tuplets with different voices aren't pretty
      const bool canInsertTuplet = (allowParallelTuplets)
                  ? needInsertTuplet : (needInsertTuplet && needDeleteOldTuplet);
      if (canInsertTuplet) {
            insertNewTuplet(tuplet, tupletOnTime, newVoice, chords, tuplets, insertedTuplets);
            isInTuplet = true;
            }
      if (needDeleteOldTuplet) {     // delete after insert, because oldTuplet can be used
            bool canRemoveTuplet = true;
            if (isThisAChord) {
                              // don't remove tuplet if chord notes have the same tuplet,
                              // it will be removed on note split
                  for (const auto &note: notes) {
                        if (note.isInTuplet && note.tuplet == oldTuplet) {
                              canRemoveTuplet = false;
                              break;
                              }
                        }
                  }
            if (canRemoveTuplet)
                  MidiTuplet::removeTuplet(oldTuplet, tuplets, maxChordLength, chords);
            }

      Q_ASSERT_X(allowParallelTuplets || !needInsertTuplet || needDeleteOldTuplet,
                 "MidiVoice::splitTuplet",
                 "Tuplet need to be added but the old tuplet was not deleted");
      }

bool updateChordTuplets(
            MidiChord &chord,
            const ReducedFraction &onTime,
            std::multimap<ReducedFraction,
                 std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator> &insertedTuplets,
            std::multimap<ReducedFraction, MidiChord> &chords,
            std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets,
            const ReducedFraction &maxChordLength,
            bool allowParallelTuplets)
      {
      if (!allowParallelTuplets) {
            bool canDoSplit = true;
            if (chord.isInTuplet && !canSplitTuplet(chord.tuplet, chord.voice, onTime,
                                                    chord.notes, insertedTuplets, chords,
                                                    maxChordLength)) {
                  canDoSplit = false;
                  }
            if (canDoSplit) {
                  for (auto &note: chord.notes) {
                        if (note.isInTuplet && !canSplitTuplet(note.tuplet, chord.voice, onTime,
                                                               {note}, insertedTuplets, chords,
                                                               maxChordLength)) {
                              canDoSplit = false;
                              break;
                              }
                        }
                  }
            if (!canDoSplit)
                  return false;
            }

      if (chord.isInTuplet) {
            splitTuplet(chord.tuplet, chord.voice, onTime,
                        chord.notes, chord.isInTuplet,
                        insertedTuplets, chords,
                        tuplets, maxChordLength, allowParallelTuplets, true);
            }
      for (auto &note: chord.notes) {
            if (note.isInTuplet) {
                  splitTuplet(note.tuplet, chord.voice, onTime,
                              {note}, note.isInTuplet,
                              insertedTuplets, chords,
                              tuplets, maxChordLength, allowParallelTuplets, false);
                  }
            }

      return true;
      }

int findMaxOccupiedVoiceInBar(
            const std::multimap<ReducedFraction, MidiChord>::iterator &chordIt,
            const std::multimap<ReducedFraction, MidiChord> &chords)
      {
      const int barIndex = chordIt->second.barIndex;
      int maxVoice = 0;
                  // look forward
      for (auto it = chordIt; it != chords.end(); ++it) {
            const MidiChord &chord = it->second;
            if (chord.barIndex > barIndex + 1)
                  break;
            if (chord.barIndex == barIndex && chord.voice > maxVoice)
                  maxVoice = chord.voice;
            }
                  // look backward
      for (auto it = chordIt; ; ) {
            const MidiChord &chord = it->second;
            if (chord.barIndex < barIndex - 1)
                  break;
            if (chord.barIndex == barIndex && chord.voice > maxVoice)
                  maxVoice = chord.voice;
            if (it == chords.begin())
                  break;
            --it;
            }

      return maxVoice;
      }

bool splitChordToVoice(
            std::multimap<ReducedFraction, MidiChord>::iterator &chordIt,
            const QSet<int> &notesToMove,
            int newVoice,
            std::multimap<ReducedFraction, MidiChord> &chords,
            std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets,
            std::multimap<ReducedFraction,
                 std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator> &insertedTuplets,
            const ReducedFraction &maxChordLength,
            bool allowParallelTuplets)
      {
      if (notesToMove.isEmpty())
            return false;

      bool splitDone = true;
      const ReducedFraction onTime = chordIt->first;
      MidiChord &chord = chordIt->second;
      auto &notes = chord.notes;

      Q_ASSERT_X(MidiTuplet::isTupletRangeOk(*chordIt, tuplets),
                 "MidiVoice::splitChordToVoice, before split",
                 "Tuplet chord/note is outside tuplet "
                 "or non-tuplet chord/note is inside tuplet before simplification");

      if (notesToMove.size() == notes.size()) {
                        // don't split chord, just move it to another voice
            const int oldVoice = chord.voice;   // remember for possible undo
            chord.voice = newVoice;

            const bool success = updateChordTuplets(chord, onTime, insertedTuplets,
                                                    chords, tuplets, maxChordLength,
                                                    allowParallelTuplets);
            if (!success) {
                  splitDone = false;
                  chord.voice = oldVoice;       // rollback
                  }
            }
      else {            // split chord
            MidiChord newChord(chord);
            newChord.notes.clear();
            newChord.voice = newVoice;
            QList<MidiNote> updatedOldNotes;

            for (int i = 0; i != notes.size(); ++i) {
                  if (notesToMove.contains(i))
                        newChord.notes.append(notes[i]);
                  else
                        updatedOldNotes.append(notes[i]);
                  }

            const auto rememberedNotes = notes;       // to undo split if necessary
                        // update notes before tuplet update because there will be check
                        // for empty tuplets
            notes = updatedOldNotes;

            const bool success = updateChordTuplets(newChord, onTime, insertedTuplets,
                                                    chords, tuplets, maxChordLength,
                                                    allowParallelTuplets);
            if (success) {

                  Q_ASSERT_X(!notes.isEmpty(),
                             "MidiVoice::splitChordToVoice", "Old chord notes are empty");
                  Q_ASSERT_X(!newChord.notes.isEmpty(),
                             "MidiVoice::splitChordToVoice", "New chord notes are empty");

                  chordIt = chords.insert({onTime, newChord});
                  }
            else {
                  notes = rememberedNotes;      // rollback
                  splitDone = false;
                  }
            }

      Q_ASSERT_X(MidiTuplet::isTupletRangeOk(*chordIt, tuplets),
                 "MidiVoice::splitChordToVoice, after split",
                 "Tuplet chord/note is outside tuplet "
                 "or non-tuplet chord/note is inside tuplet before simplification");

      return splitDone;
      }

QSet<int> findNotesToMove(const QList<MidiNote> &notes,
                          int splitPoint,
                          MovedVoiceGroup splitGroup)
      {
      QSet<int> notesToMove;
      if (splitPoint == 0 || splitPoint == notes.size()) {
                        // don't split chord, just move it to another voice
            for (int i = 0; i != notes.size(); ++i)
                  notesToMove.insert(i);
            }
      else {
            switch (splitGroup) {
                  case MovedVoiceGroup::LOW:
                        for (int i = 0; i != splitPoint; ++i)
                              notesToMove.insert(i);
                        break;
                  case MovedVoiceGroup::HIGH:
                        for (int i = splitPoint; i != notes.size(); ++i)
                              notesToMove.insert(i);
                        break;
                  }
            }
      return notesToMove;
      }

bool doVoiceSeparation(
            std::multimap<ReducedFraction, MidiChord> &chords,
            const TimeSigMap *sigmap,
            std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets)
      {

      Q_ASSERT_X(MidiTuplet::areTupletRangesOk(chords, tuplets),
                 "MidiVoice::doVoiceSeparation",
                 "Tuplet chord/note is outside tuplet "
                 "or non-tuplet chord/note is inside tuplet before voice separation");

      MChord::sortNotesByLength(chords);
      std::multimap<ReducedFraction,
                  std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator> insertedTuplets;
      const ReducedFraction maxChordLength = MChord::findMaxChordLength(chords);

      bool changed = false;
      std::map<int, int> maxOccupiedVoices;   // <bar index, max occupied voice>

      for (auto it = chords.begin(); it != chords.end(); ++it) {
            MidiChord &chord = it->second;
            auto &notes = chord.notes;

            const int splitPoint = findOptimalSplitPoint(it, sigmap, tuplets, chords);
            if (splitPoint == -1)
                  continue;

            auto maxVoiceIt = maxOccupiedVoices.find(chord.barIndex);
            if (maxVoiceIt == maxOccupiedVoices.end()) {
                  const int maxVoice = findMaxOccupiedVoiceInBar(it, chords);
                  maxVoiceIt = maxOccupiedVoices.insert({chord.barIndex, maxVoice}).first;
                  }
            const auto possibleSplits = findPossibleVoiceSplits(
                                                chord.voice, it, splitPoint, chords,
                                                tuplets, insertedTuplets, maxVoiceIt->second);
            if (possibleSplits.empty())
                  continue;

            const VoiceSplit bestSplit = findBestSplit(it, chords, possibleSplits, splitPoint);
            changed = true;

            const QSet<int> notesToMove = findNotesToMove(notes, splitPoint, bestSplit.group);
            const bool splitDone = splitChordToVoice(
                                          it, notesToMove, bestSplit.voice, chords, tuplets,
                                          insertedTuplets, maxChordLength);
            if (splitDone && bestSplit.voice > maxVoiceIt->second)
                  maxVoiceIt->second = bestSplit.voice;
            }

      Q_ASSERT_X(MidiTuplet::areTupletRangesOk(chords, tuplets),
                 "MidiVoice::doVoiceSeparation",
                 "Tuplet chord/note is outside tuplet "
                 "or non-tuplet chord/note is inside tuplet after voice separation");

      return changed;
      }

int findBarIndexForOffTime(const ReducedFraction &offTime, const TimeSigMap *sigmap)
      {
      int barIndex, beat, tick;
      sigmap->tickValues(offTime.ticks(), &barIndex, &beat, &tick);
      if (beat == 0 && tick == 0)
            --barIndex;
      return barIndex;
      }

int averagePitchOfChords(
            const std::vector<std::multimap<ReducedFraction, MidiChord>::iterator> &chords)
      {
      if (chords.empty())
            return -1;

      int sumPitch = 0;
      int noteCounter = 0;
      for (const auto &chord: chords) {
            const auto &midiNotes = chord->second.notes;
            for (const auto &midiNote: midiNotes) {
                  sumPitch += midiNote.pitch;
                  ++noteCounter;
                  }
            }

      return qRound(sumPitch * 1.0 / noteCounter);
      }

void sortVoicesByPitch(const std::map<int, std::vector<
                              std::multimap<ReducedFraction, MidiChord>::iterator>> &voiceChords)
      {
            // [newVoice] = <average pitch, old voice>
      std::vector<std::pair<int, int>> pitchVoices;
      for (const auto &v: voiceChords)
            pitchVoices.push_back({averagePitchOfChords(v.second), v.first});

      struct {
            bool operator()(const std::pair<int, int> &p1,
                            const std::pair<int, int> &p2) const
                  {
                  if (p1.first != p2.first)
                        return p1.first > p2.first;
                  return p1.second < p2.second;
                  }
            } comparator;
      std::sort(pitchVoices.begin(), pitchVoices.end(), comparator);

      for (int newVoice = 0; newVoice != (int)pitchVoices.size(); ++newVoice) {
            const int oldVoice = pitchVoices[newVoice].second;
            if (newVoice == oldVoice)
                  continue;
            const auto it = voiceChords.find(oldVoice);

            Q_ASSERT_X(it != voiceChords.end(),
                       "MidiVoice::sortVoicesByPitch", "Old voice not found");

            for (auto &chord: it->second) {
                  MidiChord &c = chord->second;
                  c.voice = newVoice;
                  if (c.isInTuplet)
                        c.tuplet->second.voice = newVoice;
                  for (auto &note: c.notes) {
                        if (note.isInTuplet)
                              note.tuplet->second.voice = newVoice;
                        }
                  }
            }
      }

void sortVoices(
            std::multimap<ReducedFraction, MidiChord> &chords,
            const TimeSigMap *sigmap)
      {
                  // <voice, chords>
      std::map<int, std::vector<std::multimap<ReducedFraction, MidiChord>::iterator>> voiceChords;
      int maxBarIndex = 0;

      for (auto it = chords.begin(); it != chords.end(); ++it) {
            const auto &chord = it->second;

            // some notes: if chord off time belongs to tuplet
            // then this tuplet belongs to the same bar as the chord off time;
            // same is for chord on time

            Q_ASSERT_X(chord.barIndex != -1,
                       "MidiVoice::sortVoices", "Chord bar index is undefined");
            Q_ASSERT_X((!voiceChords.empty()) ? chord.barIndex <= maxBarIndex : true,
                       "MidiVoice::sortVoices", "Chord bar index is greater than current index");

            voiceChords[chord.voice].push_back(it);
            const int barIndex = findBarIndexForOffTime(
                                    MChord::maxNoteOffTime(chord.notes), sigmap);
            if (barIndex > maxBarIndex)
                  maxBarIndex = barIndex;

            if (std::next(it) == chords.end() || std::next(it)->second.barIndex > maxBarIndex) {
                  sortVoicesByPitch(voiceChords);
                  voiceChords.clear();
                  }
            }
      }

bool separateVoices(std::multimap<int, MTrack> &tracks, const TimeSigMap *sigmap)
      {
      auto &opers = preferences.midiImportOperations;
      bool changed = false;

      for (auto &track: tracks) {
            MTrack &mtrack = track.second;
            if (mtrack.mtrack->drumTrack())
                  continue;
            auto &chords = track.second.chords;
            if (chords.empty())
                  continue;
            const int userVoiceCount = toIntVoiceCount(
                        opers.data()->trackOpers.maxVoiceCount.value(mtrack.indexOfOperation));
                        // pass current track index through MidiImportOperations
                        // for further usage
            MidiOperations::CurrentTrackSetter setCurrentTrack{opers, mtrack.indexOfOperation};

            if (userVoiceCount > 1 && userVoiceCount <= voiceLimit()) {

                  Q_ASSERT_X(MidiTuplet::areAllTupletsReferenced(mtrack.chords, mtrack.tuplets),
                             "MidiVoice::separateVoices",
                             "Not all tuplets are referenced in chords or notes "
                             "before voice separation");
                  Q_ASSERT_X(areVoicesSame(mtrack.chords),
                             "MidiVoice::separateVoices", "Different voices of chord and tuplet "
                             "before voice separation");

                  if (doVoiceSeparation(mtrack.chords, sigmap, mtrack.tuplets))
                        changed = true;

                  Q_ASSERT_X(MidiTuplet::areAllTupletsReferenced(mtrack.chords, mtrack.tuplets),
                             "MidiVoice::separateVoices",
                             "Not all tuplets are referenced in chords or notes "
                             "after voice separation, before voice sort");
                  Q_ASSERT_X(areVoicesSame(mtrack.chords),
                             "MidiVoice::separateVoices", "Different voices of chord and tuplet "
                             "after voice separation, before voice sort");

                  sortVoices(mtrack.chords, sigmap);

                  Q_ASSERT_X(MidiTuplet::areAllTupletsReferenced(mtrack.chords, mtrack.tuplets),
                             "MidiVoice::separateVoices",
                             "Not all tuplets are referenced in chords or notes "
                             "after voice sort");
                  Q_ASSERT_X(areVoicesSame(mtrack.chords),
                             "MidiVoice::separateVoices", "Different voices of chord and tuplet "
                             "after voice sort");
                  }
            }

      return changed;
      }

} // namespace MidiVoice
} // namespace Ms
