package com.xenoage.zong.core.header;
import com.xenoage.utils.annotations.MaybeEmpty;
import com.xenoage.utils.annotations.MaybeNull;
import com.xenoage.utils.annotations.NonNull;
import com.xenoage.utils.annotations.Untested;
import com.xenoage.utils.collections.CList;
import com.xenoage.utils.collections.IList;
import com.xenoage.utils.exceptions.UnsupportedClassException;
import com.xenoage.utils.math.Fraction;
import com.xenoage.zong.core.Score;
import com.xenoage.zong.core.format.Break;
import com.xenoage.zong.core.music.ColumnElement;
import com.xenoage.zong.core.music.MeasureSide;
import com.xenoage.zong.core.music.MusicElementType;
import com.xenoage.zong.core.music.barline.Barline;
import com.xenoage.zong.core.music.barline.BarlineRepeat;
import com.xenoage.zong.core.music.direction.*;
import com.xenoage.zong.core.music.key.Key;
import com.xenoage.zong.core.music.time.TimeSignature;
import com.xenoage.zong.core.music.util.BeatE;
import com.xenoage.zong.core.music.util.BeatEList;
import com.xenoage.zong.core.music.volta.Volta;
import com.xenoage.zong.core.position.MP;
import com.xenoage.zong.core.position.MPContainer;
import com.xenoage.zong.core.position.MPElement;
import lombok.Data;
import lombok.val;
import static com.xenoage.utils.CheckUtils.checkArgsNotNull;
import static com.xenoage.utils.CheckUtils.checkNotNull;
import static com.xenoage.utils.collections.ArrayUtils.containsRef;
import static com.xenoage.utils.collections.CList.clist;
import static com.xenoage.utils.math.Fraction._0;
import static com.xenoage.zong.core.music.util.BeatEList.beatEList;
import static com.xenoage.zong.core.position.MP.atColumnBeat;
/**
* A {@link ColumnHeader} stores information that
* is used for the whole measure column,
* i.e. key signature and time signature.
*
* The start and end barline as well as middle barlines
* are saved here, and a volta if it begins in this
* measure.
*
* There is also a list of tempo directions for this
* measure and layout break information.
*
* @author Andreas Wenger
*/
@Data
public final class ColumnHeader
implements DirectionContainer, MPContainer {
/** The time signature at the beginning of this measure. */
@MaybeNull private TimeSignature time;
/** The barline at the beginning of this measure. */
@MaybeNull private Barline startBarline;
/** The barline at the end of this measure, or null if unset. */
@MaybeNull private Barline endBarline;
/** The barlines in the middle of the measure. */
@NonNull @MaybeEmpty private BeatEList<Barline> middleBarlines;
/** The volta that begins at this measure. */
@MaybeNull private Volta volta;
/** The key signature changes in this measure. */
@NonNull @MaybeEmpty private BeatEList<Key> keys;
/** The tempo changes in this measure */
@NonNull @MaybeEmpty private BeatEList<Tempo> tempos;
/** The {@link Break} after this measure, or null. */
@MaybeNull private Break measureBreak;
/** The {@link NavigationSign} at the beginning of this measure, or null. */
@MaybeNull private Direction navigationTarget;
/** The {@link NavigationSign} at the end of this measure, or null. */
@MaybeNull private Direction navigationOrigin;
/** The other {@link Direction}s in this measure */
@NonNull @MaybeEmpty private BeatEList<Direction> otherDirections;
/** Back reference: parent score, or null if not part of a score. */
private Score parentScore = null;
/** Back reference: measure index, or null if not part of a score. */
private Integer parentMeasureIndex = null;
public ColumnHeader(Score parentScore, Integer parentMeasureIndex) {
this.time = null;
this.startBarline = null;
this.endBarline = null;
this.middleBarlines = beatEList();
this.volta = null;
this.keys = beatEList();
this.tempos = beatEList();
this.measureBreak = null;
this.otherDirections = beatEList();
this.parentScore = parentScore;
this.parentMeasureIndex = parentMeasureIndex;
}
/**
* Sets the time signature, or null if unset.
* If there is already one, it is replaced and returned (otherwise null).
*/
public TimeSignature setTime(TimeSignature time) {
TimeSignature old = this.time;
this.time = time;
this.time.setParent(this);
return old;
}
/**
* Sets the barline at the beginning of this measure, or null if unset.
* If there is already one, it is replaced and returned (otherwise null).
*/
public Barline setStartBarline(Barline startBarline) {
checkStartBarline(startBarline);
Barline old = this.startBarline;
this.startBarline = startBarline;
this.startBarline.setParent(this);
return old;
}
/**
* Sets the barline at the end of this measure, or null if unset.
* If there is already one, it is replaced and returned (otherwise null).
*/
public Barline setEndBarline(Barline endBarline) {
checkEndBarline(endBarline);
Barline old = this.endBarline;
this.endBarline = endBarline;
this.endBarline.setParent(this);
return old;
}
/**
* Sets a barline in the middle of the measure.
* If there is already one at the given beat, it is replaced and returned (otherwise null).
*/
public Barline setMiddleBarline(@NonNull Barline middleBarline, Fraction beat) {
checkArgsNotNull(middleBarline);
middleBarline.setParent(this);
return middleBarlines.set(middleBarline, beat);
}
/**
* Removes a barline in the middle of the measure.
* If found, is returned (otherwise null).
*/
public Barline removeMiddleBarline(Fraction beat) {
Barline ret = middleBarlines.remove(beat);
if (ret != null)
ret.setParent(null);
return ret;
}
/**
* Sets the volta beginning at this measure, or null if unset.
* If there is already one, it is replaced and returned (otherwise null).
*/
public Volta setVolta(Volta volta) {
Volta old = this.volta;
this.volta = volta;
this.volta.setParent(this);
return old;
}
/**
* Sets a key in this measure.
* If there is already one at the given beat, it is replaced and returned (otherwise null).
*/
public Key setKey(Key key, Fraction beat) {
checkArgsNotNull(key, beat);
key.setParent(this);
return keys.set(key, beat);
}
/**
* Removes a key from this measure.
* If found, it is returned (otherwise null).
*/
public Key removeKey(Fraction beat) {
Key ret = keys.remove(beat);
if (ret != null)
ret.setParent(null);
return ret;
}
/**
* Sets a tempo in this measure.
* If there is already one at the given beat, it is replaced and returned (otherwise null).
*/
public Tempo setTempo(Tempo tempo, Fraction beat) {
checkArgsNotNull(tempo, beat);
tempo.setParent(this);
return tempos.set(tempo, beat);
}
/**
* Removes a tempo from this measure.
* If found, it is returned (otherwise null).
*/
public Tempo removeTempo(Fraction beat) {
Tempo ret = tempos.remove(beat);
if (ret != null)
ret.setParent(null);
return ret;
}
/**
* Sets the {@link Break} after this measure, or null if there is none.
* If there is already one, it is replaced and returned (otherwise null).
*/
public Break setBreak(Break measureBreak) {
Break old = this.measureBreak;
this.measureBreak = measureBreak;
if (this.measureBreak != null)
this.measureBreak.setParent(this);
return old;
}
/**
* Sets the {@link NavigationSign} at the beginning of a measure
* into which can be jumped, e.g. a jump indicated by "segno" or "coda".
* If there is already one, it is replaced and returned (otherwise null).
* Setting a "capo" is not possible here.
*/
public Direction setNavigationTarget(NavigationSign sign) {
if (sign instanceof DaCapo)
throw new IllegalArgumentException("DaCapo can not be a navigation target");
val old = this.navigationTarget;
this.navigationTarget = (Direction) sign;
return old;
}
/**
* Sets the {@link NavigationSign} to perform after this measure,
* e.g. a jump indicated by "da capo", "dal segno" or "to coda".
* If there is already one, it is replaced and returned (otherwise null).
*/
public Direction setNavigationOrigin(NavigationSign sign) {
val old = this.navigationOrigin;
this.navigationOrigin = (Direction) sign;
return old;
}
/**
* Adds the given {@link Direction} to this measure.
* For a {@link Tempo}, use {@link #setTempo(Tempo, Fraction)} with a null value instead.
* For a {@link NavigationSign}, use #setNavigationTarget or #setNavigationOrigin instead.
*/
public void addOtherDirection(Direction direction, Fraction beat) {
checkArgsNotNull(direction, beat);
if (containsRef(middleNotAllowedDirections, direction.getMusicElementType()))
throw new IllegalArgumentException("direction type not allowed at this position");
direction.setParent(this);
otherDirections.add(direction, beat);
}
private static MusicElementType[] middleNotAllowedDirections = {
MusicElementType.Tempo, MusicElementType.Coda, MusicElementType.DaCapo, MusicElementType.Segno
};
/**
* Removes the given {@link Direction} from this measure.
* If found, it is returned (otherwise null).
* For a {@link Tempo}, use {@link #removeTempo(Fraction)} instead.
*/
public Direction removeOtherDirection(Direction direction) {
Direction ret = otherDirections.remove(direction);
if (ret != null)
ret.setParent(null);
return ret;
}
/**
* Checks the given start barline.
*/
private void checkStartBarline(Barline startBarline) {
//both side repeat is not allowed
if (startBarline != null && startBarline.getRepeat() == BarlineRepeat.Both)
throw new IllegalArgumentException(BarlineRepeat.Both +
" is not supported for a start barline.");
}
/**
* Checks the given end barline.
*/
private void checkEndBarline(Barline endBarline) {
//both side repeat is not allowed
if (endBarline != null && endBarline.getRepeat() == BarlineRepeat.Both)
throw new IllegalArgumentException(BarlineRepeat.Both +
" is not supported for an end barline.");
}
/**
* Sets the given {@link ColumnElement} at the given beat.
*
* If there is already another element of this type, it is replaced and returned (otherwise null),
* except for {@link Direction}s (except {@link Tempo}), where multiple elements on a single beat
* are allowed.
*
* @param element the element to add
* @param beat the beat where to add the element. Only needed for
* key, tempo, middle barlines and other directions
* @param side Only needed for barlines
*/
@Untested public ColumnElement setColumnElement(ColumnElement element, @MaybeNull Fraction beat,
@MaybeNull MeasureSide side) {
switch (element.getMusicElementType()) {
case Barline: {
Barline barline = (Barline) element;
//left or right barline
if (side == MeasureSide.Left)
return setStartBarline(barline);
else if (side == MeasureSide.Right)
return setEndBarline(barline);
//middle barline
checkNotNull(beat);
return setMiddleBarline(barline, beat);
}
case Break: {
return setBreak((Break) element);
}
case TraditionalKey: {
checkNotNull(beat);
return setKey((Key) element, beat);
}
case Tempo: {
checkNotNull(beat);
return setTempo((Tempo) element, beat);
}
case Time: {
return setTime((TimeSignature) element);
}
case Volta: {
return setVolta((Volta) element);
}
case Coda: case DaCapo: case Segno: {
//write at beginning of measure (jump target), when beat is 0,
//otherwise write to end of measure (jump origin)
if (beat.is0())
return setNavigationTarget((NavigationSign) element);
else
return setNavigationOrigin((NavigationSign) element);
}
default: {
if (element instanceof Direction) {
addOtherDirection((Direction) element, beat);
return null;
}
}
}
throw new UnsupportedClassException(element);
}
/**
* Removes the given {@link ColumnElement}.
*/
@Untested public void removeColumnElement(ColumnElement element) {
if (element instanceof Barline) {
//left or right barline
if (element == startBarline)
startBarline = null;
else if (element == endBarline)
endBarline = null;
//middle barline
else
middleBarlines.remove((Barline) element);
}
else if (element instanceof Break) {
measureBreak = null;
}
else if (element instanceof Key) {
keys.remove((Key) element);
}
else if (element instanceof Tempo) {
tempos.remove((Tempo) element);
}
else if (element instanceof TimeSignature) {
time = null;
}
else if (element instanceof Volta) {
volta = null;
}
else if (element instanceof Direction) {
otherDirections.remove((Direction) element);
}
else {
throw new UnsupportedClassException(element);
}
}
/**
* Replaces the given {@link ColumnElement} with the other given one.
*/
@Untested public <T extends ColumnElement> void replaceColumnElement(T oldElement, T newElement) {
if (newElement instanceof Barline) {
Barline newBarline = (Barline) newElement;
//left or right barline
if (oldElement == startBarline)
setStartBarline(newBarline);
else if (oldElement == endBarline)
setEndBarline(newBarline);
else {
//middle barline
for (BeatE<Barline> middleBarline : middleBarlines) {
if (middleBarline.getElement() == oldElement) {
setMiddleBarline(newBarline, middleBarline.getBeat());
return;
}
}
throw new IllegalArgumentException("Old barline not part of this column");
}
}
else if (newElement instanceof Break) {
setBreak((Break) newElement);
}
else if (newElement instanceof Key) {
for (BeatE<Key> key : keys) {
if (key.getElement() == oldElement) {
setKey((Key) newElement, key.getBeat());
return;
}
}
throw new IllegalArgumentException("Old key not part of this column");
}
else if (newElement instanceof Tempo) {
for (BeatE<Tempo> tempo : tempos) {
if (tempo.getElement() == oldElement) {
setTempo((Tempo) newElement, tempo.getBeat());
return;
}
}
throw new IllegalArgumentException("Old tempo not part of this column");
}
else if (newElement instanceof TimeSignature) {
setTime((TimeSignature) newElement);
}
else if (newElement instanceof Volta) {
setVolta((Volta) newElement);
}
else {
throw new UnsupportedClassException(newElement);
}
}
/**
* Gets a list of all {@link ColumnElement}s in this column, which
* are assigned to a beat (middle barlines, keys and tempos).
*/
public BeatEList<ColumnElement> getColumnElementsWithBeats() {
BeatEList<ColumnElement> ret = beatEList();
ret.addAll(middleBarlines);
ret.addAll(keys);
ret.addAll(tempos);
return ret;
}
/**
* Gets a list of all {@link ColumnElement}s in this column, which
* are not assigned to a beat (time, start and end barline, volta, measure break).
*/
public IList<ColumnElement> getColumnElementsWithoutBeats() {
//elements with beats
CList<ColumnElement> ret = clist();
if (time != null)
ret.add(time);
if (startBarline != null)
ret.add(startBarline);
if (endBarline != null)
ret.add(endBarline);
if (volta != null)
ret.add(volta);
if (measureBreak != null)
ret.add(measureBreak);
return ret.close();
}
/**
* Gets a list of all {@link ColumnElement}s in this column.
*/
public IList<ColumnElement> getColumnElements() {
CList<ColumnElement> ret = clist();
if (time != null)
ret.add(time);
if (startBarline != null)
ret.add(startBarline);
if (endBarline != null)
ret.add(endBarline);
for (BeatE<Barline> e : middleBarlines)
ret.add(e.getElement());
if (volta != null)
ret.add(volta);
for (BeatE<Key> e : keys)
ret.add(e.getElement());
for (BeatE<Tempo> e : tempos)
ret.add(e.getElement());
if (measureBreak != null)
ret.add(measureBreak);
return ret.close();
}
/**
* Gets the {@link MP} of the given {@link ColumnElement}, or null if it is not part
* of this column or this column is not part of a score.
*/
@Override public MP getChildMP(MPElement element) {
if (parentScore == null || parentMeasureIndex == null)
return null;
//elements at the beginning of the measure
if (time == element || startBarline == element || volta == element)
return atColumnBeat(parentMeasureIndex, _0);
//elements at the end of the measure
else if (endBarline == element || measureBreak == element)
return atColumnBeat(parentMeasureIndex, parentScore.getMeasureBeats(parentMeasureIndex));
//elements in the middle of the measure
else if (element instanceof Barline)
return getMPIn(element, middleBarlines);
else if (element instanceof Key)
return getMPIn(element, keys);
else if (element instanceof Tempo)
return getMPIn(element, tempos);
return null;
}
/**
* Gets the {@link MP} of the given element within the given list of elements,
* or null if the list of elements is null or the element could not be found.
*/
private MP getMPIn(MPElement element, BeatEList<?> elements) {
if (elements == null)
return null;
for (BeatE<?> e : elements)
if (e.getElement() == element)
return atColumnBeat(parentMeasureIndex, e.getBeat());
return null;
}
/**
* Gets the {@link MeasureSide} of the given element in this column. This applies only to
* start and end barlines. For all other elements, null is returned.
*/
public MeasureSide getSide(ColumnElement element) {
if (element == startBarline)
return MeasureSide.Left;
else if (element == endBarline)
return MeasureSide.Right;
else
return null;
}
}