package com.xenoage.zong.commands.core.music;
import com.xenoage.utils.document.command.Command;
import com.xenoage.utils.document.command.Undoability;
import com.xenoage.utils.math.Fraction;
import com.xenoage.zong.core.Score;
import com.xenoage.zong.core.music.Voice;
import com.xenoage.zong.core.music.VoiceElement;
import com.xenoage.zong.core.music.rest.Rest;
import com.xenoage.zong.core.music.time.TimeSignature;
import com.xenoage.zong.core.position.MP;
import com.xenoage.zong.utils.exceptions.MeasureFullException;
import java.util.ArrayList;
import java.util.List;
import static com.xenoage.utils.NullUtils.notNull;
import static com.xenoage.utils.iterators.ReverseIterator.reverseIt;
import static com.xenoage.utils.kernel.Range.rangeReverse;
import static com.xenoage.zong.core.position.MP.atElement;
/**
* Replaces the {@link VoiceElement}s between the given {@link MP}
* and the given duration (if any) by the given {@link VoiceElement}.
* If the end position is within an element, that element is removed too.
* All affected elements (slurs, beams, ...) will be removed.
*
* @author Andreas Wenger
*/
public class VoiceElementWrite
implements Command {
/**
* Options for writing.
*/
public static class Options {
/** True, when an exception should be thrown when the element
* is too long for the current time signature. If this is true, the
* given voice must be part of a score.
*/
public boolean checkTimeSignature = false;
/**
* True, if additional rests, written before the given element,
* should be set to invisible.
*/
public boolean fillWithHiddenRests = false;
}
private final static Options defaultOptions = new Options();
//data
private Voice voice;
private MP startMP; //beat or element must be given
private VoiceElement element;
private Options options;
//backup
private List<Command> backupCmds = null;
/**
* Creates a new {@link VoiceElement} command.
* @param voice the affected voice
* @param startMP position where to write the element.<ul>
* <li>If a beat is given: If there is empty space before this beat,
* it is filled by rests. If this beat is within an element, not this position
* but the start position of the element is used.</li>
* <li>If an element index is given: The element is written
* at the start beat of the existing element with this index, or, if it does not exist,
* after the last element in this voice.</li></ul>
* @param element the element to write
* @param options options for writing, or null for the default settings
*/
public VoiceElementWrite(Voice voice, MP startMP, VoiceElement element, Options options) {
this.voice = voice;
this.startMP = startMP;
this.element = element;
this.options = notNull(options, defaultOptions);
}
@Override public void execute()
throws MeasureFullException {
//determine start mp and element index
Fraction startBeat;
int elementIndex;
if (startMP.element != MP.unknown) {
//start at indexed element
elementIndex = startMP.element;
startBeat = voice.getBeat(elementIndex);
}
else if (startMP.beat != MP.unknownBeat) {
//start at given beat
Fraction filledBeats = voice.getFilledBeats();
startBeat = startMP.beat;
Fraction emptySpace = startBeat.sub(filledBeats);
if (emptySpace.isGreater0()) {
//add rest between start beat and filled beats, if needed
//TODO: split rests into reasonable parts
Rest rest = new Rest(emptySpace);
rest.setHidden(options.fillWithHiddenRests);
executeAndRemember(new VoiceElementWrite(voice, atElement(voice.getElements().size()),
rest, null));
startBeat = startBeat.add(emptySpace);
elementIndex = voice.getElements().size();
}
else {
elementIndex = voice.getElementIndex(startBeat);
}
//update start beat (may be within an element before)
startBeat = voice.getBeat(elementIndex);
}
else {
throw new IllegalArgumentException("element index or beat must be given");
}
//affected range (start and end beat)
Fraction endBeat = startBeat.add(element.getDuration());
//optionally check time signature
if (options.checkTimeSignature) {
Score score = voice.getScore();
if (score == null)
throw new IllegalStateException("parent score is required");
TimeSignature time = score.getHeader().getTimeAtOrBefore(startMP.getMeasure());
Fraction duration = time.getType().getMeasureBeats();
if (duration != null && endBeat.compareTo(duration) > 0) {
throw new MeasureFullException(startMP, element.getDuration());
}
}
//remove elements within the range
Fraction posBeat = startBeat;
int firstRemoveIndex = elementIndex;
int lastRemoveIndex = -1;
for (int i = firstRemoveIndex; i < voice.getElements().size() && posBeat.compareTo(endBeat) < 0; i++) {
//we are still not at the end beat. remove element
VoiceElement e = voice.getElements().get(i);
posBeat = posBeat.add(e.getDuration());
lastRemoveIndex = i;
}
for (int i : rangeReverse(lastRemoveIndex, firstRemoveIndex)) {
executeAndRemember(new VoiceElementRemove(voice.getElements().get(i)));
}
//insert new element
voice.addElement(elementIndex, element);
}
@Override public Undoability getUndoability() {
return Undoability.Undoable;
}
@Override public void undo() {
voice.removeElement(element);
if (backupCmds != null) {
for (Command cmd : reverseIt(backupCmds))
cmd.undo();
}
}
private void executeAndRemember(Command cmd) {
if (backupCmds == null)
backupCmds = new ArrayList<>();
cmd.execute();
backupCmds.add(cmd);
}
}