/* This file is part of the KDE project
 * SPDX-FileCopyrightText: 2007 Marijn Kruisselbrink <mkruisselbrink@kde.org>
 *
 * SPDX-License-Identifier: LGPL-2.0-or-later
 */
#include "Chord.h"
#include "Bar.h"
#include "Clef.h"
#include "Note.h"
#include "Sheet.h"
#include "Staff.h"
#include "VoiceBar.h"

#include <QList>

#include <climits>

namespace MusicCore
{

namespace
{
struct Beam {
    Beam(Chord *chord)
        : beamStart(chord)
        , beamEnd(chord)
        , beamType(BeamFlag)
    {
    }
    Chord *beamStart;
    Chord *beamEnd;
    BeamType beamType;
};
}

class Chord::Private
{
public:
    Duration duration;
    int dots;
    QList<Note *> notes;
    StemDirection stemDirection;
    qreal stemLength;
    QList<Beam> beams;
};

static qreal calcStemLength(Duration duration)
{
    switch (duration) {
    case BreveNote:
    case WholeNote:
        return 0;
    case HalfNote:
    case QuarterNote:
    case EighthNote:
        return 3.5;
    case SixteenthNote:
        return 4;
    case ThirtySecondNote:
        return 4.75;
    case SixtyFourthNote:
        return 5.5;
    case HundredTwentyEighthNote:
        return 6.25;
    }
    return 0;
}

Chord::Chord(Duration duration, int dots)
    : VoiceElement()
    , d(new Private)
{
    d->duration = duration;
    d->dots = dots;
    d->stemLength = calcStemLength(duration);
    d->stemDirection = StemUp;

    int baseLength = durationToTicks(duration);
    int length = baseLength;
    for (int i = 0; i < dots; i++) {
        length += baseLength >> (i + 1);
    }
    setLength(length);
}

Chord::Chord(Staff *staff, Duration duration, int dots)
    : d(new Private)
{
    d->duration = duration;
    d->dots = dots;
    d->stemLength = calcStemLength(duration);
    d->stemDirection = StemUp;

    int baseLength = durationToTicks(duration);
    int length = baseLength;
    for (int i = 0; i < dots; i++) {
        length += baseLength >> (i + 1);
    }
    setLength(length);
    setStaff(staff);
}

Chord::~Chord()
{
    delete d;
}

qreal Chord::width() const
{
    qreal w = 7;

    int lastPitch = INT_MIN;
    bool hasConflict = false;
    bool haveAccidentals = false;

    foreach (Note *n, d->notes) {
        int pitch = n->pitch();
        if (pitch == lastPitch + 1) {
            hasConflict = true;
        }
        lastPitch = pitch;

        if (n->drawAccidentals()) {
            haveAccidentals = true;
        }
    }

    if (hasConflict)
        w += 6;

    if (d->dots) {
        w += 2 + 3 * d->dots;
    }

    if (haveAccidentals) {
        w += 10;
    }

    return w;
}

qreal Chord::beatline() const
{
    qreal bl = 0;

    int lastPitch = INT_MIN;
    bool hasConflict = false;
    bool haveAccidentals = false;

    foreach (Note *n, d->notes) {
        int pitch = n->pitch();
        if (pitch == lastPitch + 1) {
            hasConflict = true;
        }
        lastPitch = pitch;

        if (n->drawAccidentals()) {
            haveAccidentals = true;
        }
    }

    if (hasConflict)
        bl += 6;
    if (haveAccidentals) {
        bl += 10;
    }

    return bl;
}

Duration Chord::duration() const
{
    return d->duration;
}

void Chord::setDuration(Duration duration)
{
    if (d->duration == duration)
        return;
    d->duration = duration;
    d->stemLength = calcStemLength(duration);
    int baseLength = durationToTicks(d->duration);
    int length = baseLength;
    for (int i = 0; i < d->dots; i++) {
        length += baseLength >> (i + 1);
    }
    setLength(length);
    Q_EMIT durationChanged(duration);
}

int Chord::dots() const
{
    return d->dots;
}

void Chord::setDots(int dots)
{
    if (d->dots == dots)
        return;
    d->dots = dots;
    int baseLength = durationToTicks(d->duration);
    int length = baseLength;
    for (int i = 0; i < dots; i++) {
        length += baseLength >> (i + 1);
    }
    setLength(length);
    Q_EMIT dotsChanged(dots);
}

int Chord::noteCount() const
{
    return d->notes.size();
}

Note *Chord::note(int index) const
{
    Q_ASSERT(index >= 0 && index < noteCount());
    return d->notes[index];
}

Note *Chord::addNote(Staff *staff, int pitch, int accidentals)
{
    Note *n = new Note(this, staff, pitch, accidentals);
    addNote(n);
    return n;
}

void Chord::addNote(Note *note)
{
    Q_ASSERT(note);
    note->setParent(this);
    if (!staff())
        setStaff(note->staff());
    for (int i = 0; i < d->notes.size(); i++) {
        if (d->notes[i]->pitch() > note->pitch()) {
            d->notes.insert(i, note);
            return;
        }
    }
    d->notes.append(note);
}

void Chord::removeNote(int index, bool deleteNote)
{
    Q_ASSERT(index >= 0 && index < noteCount());
    Note *n = d->notes.takeAt(index);
    if (deleteNote) {
        delete n;
    }
}

void Chord::removeNote(Note *note, bool deleteNote)
{
    Q_ASSERT(note);
    int index = d->notes.indexOf(note);
    Q_ASSERT(index != -1);
    removeNote(index, deleteNote);
}

qreal Chord::y() const
{
    if (d->notes.size() == 0) {
        return staff()->lineSpacing();
    }

    qreal top = 1e9;
    Clef *clef = staff()->lastClefChange(voiceBar()->bar(), 0);

    foreach (Note *n, d->notes) {
        int line = 10;
        if (clef)
            line = clef->pitchToLine(n->pitch());

        Staff *s = n->staff();
        line--;
        qreal y = s->top() + line * s->lineSpacing() / 2;
        if (y < top)
            top = y;
    }
    if (staff())
        top -= staff()->top();
    return top;
}

qreal Chord::height() const
{
    if (d->notes.size() == 0) {
        return staff()->lineSpacing() * 2;
    }

    qreal top = 1e9;
    qreal bottom = -1e9;
    Clef *clef = staff()->lastClefChange(voiceBar()->bar(), 0);

    foreach (Note *n, d->notes) {
        int line = 10;
        if (clef)
            line = clef->pitchToLine(n->pitch());

        Staff *s = n->staff();
        line--;
        qreal y = s->top() + line * s->lineSpacing() / 2;
        if (y < top)
            top = y;
        line += 2;
        y = s->top() + line * s->lineSpacing() / 2;
        if (y > bottom)
            bottom = y;
    }
    if (staff()) {
        top -= staff()->top();
        bottom -= staff()->top();
    }
    return bottom - top;
}

qreal Chord::stemX() const
{
    int lastPitch = INT_MIN;
    bool hasConflict = false;
    bool haveAccidentals = false;

    foreach (Note *n, d->notes) {
        int pitch = n->pitch();
        if (pitch == lastPitch + 1) {
            hasConflict = true;
        }
        lastPitch = pitch;

        if (n->drawAccidentals()) {
            haveAccidentals = true;
        }
    }

    if (hasConflict) {
        return x() + 6 + (haveAccidentals ? 10 : 0);
    } else {
        return x() + (d->stemDirection == StemUp ? 6 : 0) + (haveAccidentals ? 10 : 0);
    }
}

qreal Chord::centerX() const
{
    return x() + 3;
}

qreal Chord::topNoteY() const
{
    if (d->notes.size() == 0) {
        return staff()->lineSpacing() * 2 + staff()->top();
    }

    qreal top = 1e9;
    Clef *clef = staff()->lastClefChange(voiceBar()->bar(), 0);

    foreach (Note *n, d->notes) {
        int line = 10;
        if (clef)
            line = clef->pitchToLine(n->pitch());

        Staff *s = n->staff();
        qreal y = s->top() + line * s->lineSpacing() / 2;
        if (y < top)
            top = y;
    }
    return top;
}

qreal Chord::bottomNoteY() const
{
    if (d->notes.size() == 0) {
        return staff()->lineSpacing() * 2 + staff()->top();
    }

    qreal bottom = -1e9;
    Clef *clef = staff()->lastClefChange(voiceBar()->bar(), 0);

    foreach (Note *n, d->notes) {
        int line = 10;
        if (clef)
            line = clef->pitchToLine(n->pitch());

        Staff *s = n->staff();
        qreal y = s->top() + line * s->lineSpacing() / 2;
        if (y > bottom)
            bottom = y;
    }
    return bottom;
}

qreal Chord::stemEndY(bool interpolateBeams) const
{
    if (d->notes.size() == 0)
        return staff()->center();

    if (beamType(0) == BeamContinue && interpolateBeams) {
        // in the middle of a beam, interpolate stem length from beam
        qreal sx = beamStart(0)->stemX(), ex = beamEnd(0)->stemX();
        qreal sy = beamStart(0)->stemEndY(), ey = beamEnd(0)->stemEndY();
        qreal dydx = (ey - sy) / (ex - sx);

        return (stemX() - sx) * dydx + sy;
    }

    Staff *topStaff = nullptr;
    Staff *bottomStaff = nullptr;
    qreal top = 1e9, bottom = -1e9;
    Clef *clef = staff()->lastClefChange(voiceBar()->bar(), 0);

    foreach (Note *n, d->notes) {
        int line = 10;
        if (clef)
            line = clef->pitchToLine(n->pitch());

        Staff *s = n->staff();
        qreal y = s->top() + line * s->lineSpacing() / 2;
        if (y > bottom) {
            bottom = y;
            bottomStaff = s;
        }
        if (y < top) {
            top = y;
            topStaff = s;
        }
    }

    Q_ASSERT(topStaff);
    Q_ASSERT(bottomStaff);

    if (stemDirection() == StemUp) {
        qreal pos = top - topStaff->lineSpacing() * stemLength();
        if (pos > topStaff->center() && beamType(0) == BeamFlag) {
            pos = topStaff->center();
        }
        return pos;
    } else {
        qreal pos = bottom + bottomStaff->lineSpacing() * stemLength();
        if (pos < bottomStaff->center() && beamType(0) == BeamFlag) {
            pos = bottomStaff->center();
        }
        return pos;
    }
}

qreal Chord::beamDirection() const
{
    if (beamType(0) == BeamStart || beamType(0) == BeamEnd || beamType(0) == BeamContinue) {
        qreal sx = beamStart(0)->stemX(), ex = beamEnd(0)->stemX();
        qreal sy = beamStart(0)->stemEndY(), ey = beamEnd(0)->stemEndY();
        qreal dydx = (ey - sy) / (ex - sx);
        return dydx;
    } else {
        return 0;
    }
}

StemDirection Chord::stemDirection() const
{
    return d->stemDirection;
}

void Chord::setStemDirection(StemDirection direction)
{
    d->stemDirection = direction;
}

StemDirection Chord::desiredStemDirection() const
{
    VoiceBar *vb = voiceBar();
    Bar *bar = vb->bar();
    int barIdx = bar->sheet()->indexOfBar(bar);

    int topLine = 0, bottomLine = 0;
    qreal topy = 1e9, bottomy = -1e9;
    for (int n = 0; n < noteCount(); n++) {
        Note *note = this->note(n);
        Staff *s = note->staff();
        Clef *clef = s->lastClefChange(barIdx);
        int line = clef->pitchToLine(note->pitch());
        qreal ypos = s->top() + line * s->lineSpacing() / 2;
        if (ypos < topy) {
            topy = ypos;
            topLine = line;
        }
        if (ypos > bottomy) {
            bottomy = ypos;
            bottomLine = line;
        }
    }
    qreal center = (bottomLine + topLine) * 0.5;
    return (center < 4 ? StemDown : StemUp);
}

qreal Chord::stemLength() const
{
    return d->stemLength;
}

void Chord::setStemLength(qreal stemLength)
{
    d->stemLength = stemLength;
}

qreal Chord::desiredStemLength() const
{
    return calcStemLength(d->duration);
}

int Chord::beamCount() const
{
    switch (d->duration) {
    case HundredTwentyEighthNote:
        return 5;
    case SixtyFourthNote:
        return 4;
    case ThirtySecondNote:
        return 3;
    case SixteenthNote:
        return 2;
    case EighthNote:
        return 1;
    default:
        return 0;
    }
}

const Chord *Chord::beamStart(int index) const
{
    if (d->beams.size() <= index)
        return this;
    return d->beams[index].beamStart;
}

const Chord *Chord::beamEnd(int index) const
{
    if (d->beams.size() <= index)
        return this;
    return d->beams[index].beamEnd;
}

Chord *Chord::beamStart(int index)
{
    if (d->beams.size() <= index)
        return this;
    return d->beams[index].beamStart;
}

Chord *Chord::beamEnd(int index)
{
    if (d->beams.size() <= index)
        return this;
    return d->beams[index].beamEnd;
}

BeamType Chord::beamType(int index) const
{
    if (d->beams.size() <= index)
        return BeamFlag;
    return d->beams[index].beamType;
}

void Chord::setBeam(int index, Chord *beamStart, Chord *beamEnd, BeamType type)
{
    Q_ASSERT(index < beamCount());
    while (d->beams.size() <= index) {
        d->beams.append(Beam(this));
    }
    d->beams[index].beamStart = beamStart;
    d->beams[index].beamEnd = beamEnd;
    if (beamStart == this && beamEnd == this) {
        if (type != BeamFlag && type != BeamForwardHook && type != BeamBackwardHook)
            type = BeamFlag;
        d->beams[index].beamType = type;
    } else if (beamStart == this)
        d->beams[index].beamType = BeamStart;
    else if (beamEnd == this)
        d->beams[index].beamType = BeamEnd;
    else
        d->beams[index].beamType = BeamContinue;
}

} // namespace MusicCore
