/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */

/*
    Rosegarden
    A MIDI and audio sequencer and musical notation editor.
    Copyright 2000-2026 the Rosegarden development team.

    This file is Copyright 2009
        Immanuel Litzroth         <immanuel203@gmail.com>

    Other copyrights also apply to some parts of this work.  Please
    see the AUTHORS file and individual file headers for details.

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License as
    published by the Free Software Foundation; either version 2 of the
    License, or (at your option) any later version.  See the file
    COPYING included with this distribution for more information.
*/

#define RG_MODULE_STRING "[TranzportClient]"

#include "base/Exception.h"
#include "commands/edit/AddMarkerCommand.h"
#include "TranzportClient.h"
#include "document/RosegardenDocument.h"
#include "document/CommandHistory.h"
#include "gui/application/CompositionPosition.h"
#include "gui/editors/segment/TrackButtons.h"
#include "RosegardenMainWindow.h"
#include "misc/Debug.h"
#include "misc/Strings.h"

#include <QSocketNotifier>

#include <errno.h>
#include <sstream>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <limits>

namespace Rosegarden
{

TranzportClient::TranzportClient(RosegardenMainWindow* rgGUIApp) :
    QObject(),
    device_online(true),
    m_rgGUIApp(rgGUIApp),
    m_rgDocument(RosegardenDocument::currentDocument),
    m_composition(&m_rgDocument->getComposition())
{
    m_descriptor = open("/dev/tranzport0",O_RDWR);

    if (m_descriptor < 0) {
        throw Exception(qstrtostr(QObject::tr("Failed to open tranzport device /dev/tranzport0")));
    }

    fcntl(m_descriptor,F_SETOWN, getpid());
    int socketFlags = fcntl(m_descriptor, F_GETFL, 0);
    if (socketFlags != -1) {
        fcntl(m_descriptor, F_SETFL, socketFlags | O_NONBLOCK);
    }

    m_socketReadNotifier = new QSocketNotifier(m_descriptor, QSocketNotifier::Read, nullptr);
    m_socketWriteNotifier = new QSocketNotifier(m_descriptor, QSocketNotifier::Write,nullptr);

    connect(m_socketReadNotifier, &QSocketNotifier::activated, this, &TranzportClient::readData);
    connect(m_socketWriteNotifier, &QSocketNotifier::activated, this, &TranzportClient::writeCommandQueue);

    connect(this, &TranzportClient::play,
            m_rgGUIApp, &RosegardenMainWindow::slotPlay );
    connect(this, &TranzportClient::stop,
            m_rgGUIApp, &RosegardenMainWindow::slotStop );
    connect(this, &TranzportClient::record,
            m_rgGUIApp, &RosegardenMainWindow::slotRecord );
    connect(this, &TranzportClient::rewind,
            m_rgGUIApp, &RosegardenMainWindow::slotRewind );
    connect(this, &TranzportClient::rewindToBeginning,
            m_rgGUIApp, &RosegardenMainWindow::slotRewindToBeginning );
    connect(this, &TranzportClient::fastForward,
            m_rgGUIApp, &RosegardenMainWindow::slotFastforward );
    connect(this, &TranzportClient::fastForwardToEnd,
            m_rgGUIApp, &RosegardenMainWindow::slotFastForwardToEnd );
    connect(this, &TranzportClient::toggleRecord,
            m_rgGUIApp, &RosegardenMainWindow::slotToggleRecord );
    connect(this, &TranzportClient::trackDown,
            m_rgGUIApp, &RosegardenMainWindow::slotSelectNextTrack);
    connect(this, &TranzportClient::trackUp,
            m_rgGUIApp, &RosegardenMainWindow::slotSelectPreviousTrack);
    connect(this, &TranzportClient::trackMute,
            m_rgGUIApp, &RosegardenMainWindow::slotToggleMute);
    connect(this, &TranzportClient::trackRecord,
            m_rgGUIApp, &RosegardenMainWindow::slotToggleRecordCurrentTrack );
    connect(this, &TranzportClient::solo,
            m_rgGUIApp, &RosegardenMainWindow::slotToggleSolo);

    connect(m_rgGUIApp, &RosegardenMainWindow::documentLoaded,
            this, &TranzportClient::slotDocumentLoaded);

    connect(CompositionPosition::getInstance(),
            &CompositionPosition::changed,
            this, &TranzportClient::pointerPositionChanged);

    connect(m_rgDocument, &RosegardenDocument::loopChanged,
            this, &TranzportClient::loopChanged);

    connect(this, &TranzportClient::undo,
            CommandHistory::getInstance(),&CommandHistory::undo);

    connect(this, &TranzportClient::redo,
            CommandHistory::getInstance(), &CommandHistory::redo);

    connect(this, &TranzportClient::setPosition,
            CompositionPosition::getInstance(),
            &CompositionPosition::slotSet);

    m_composition->addObserver(this);
    m_socketWriteNotifier->setEnabled(false);
    stateUpdate();

    RG_DEBUG << "TranzportClient::TranzportClient: connected to tranzport device: " << m_descriptor;
}

void
TranzportClient::pointerPositionChanged(timeT time)
{
    RG_DEBUG << "TranzportClient, pointerPositionChanged";

    if (device_online) {
        static int prevbeat = 0;
        int bar, beat, fraction, remainder;

        m_composition->getMusicalTimeForAbsoluteTime(time,bar,beat,fraction,remainder);
        if (prevbeat != beat) {
            std::stringstream ss;
            ss << bar+1 << ":" << beat;
            LCDWrite(ss.str(), Bottom, 10);
            prevbeat = beat;
        }
    }
}

void
TranzportClient::slotDocumentLoaded(RosegardenDocument *doc)
{
    RG_DEBUG << "TranzportClient::DocumentChanged ";

    m_rgDocument = doc;
    m_composition = &m_rgDocument->getComposition();
    m_composition->addObserver(this);
    connect(CompositionPosition::getInstance(),
            &CompositionPosition::changed,
            this, &TranzportClient::pointerPositionChanged);
    connect(m_rgDocument, &RosegardenDocument::loopChanged,
            this, &TranzportClient::loopChanged);
    connect(this, &TranzportClient::setPosition,
            CompositionPosition::getInstance(),
            &CompositionPosition::slotSet);

    while (not commands.empty()) {
        commands.pop();
    }
    stateUpdate();
}

#if 0
// soloChanged() has been removed.  Solo has moved from Composition to
// Track.  The new CompositionObserver::selectedTrackChanged() might be
// of interest in bringing this back to life.

/**
 * Called when solo status changes (solo on/off, and selected track)
 */
void
TranzportClient::soloChanged(const Composition * c,
                             bool  solo,
                             TrackId  selectedTrack )
{
    RG_DEBUG << "TranzportClient, CompositionObserver::soloChanged";

    if (device_online) {
        if (solo) {
            LightOn(LightAnysolo);
        } else {
            LightOff(LightAnysolo);
        }

        Track* track = c->getTrackById(selectedTrack);

        if (track->isArmed()) {
            LightOn(LightTrackrec);
        } else {
            LightOff(LightTrackrec);
        }

        if (track->isMuted()) {
            LightOn(LightTrackmute);
        } else {
            LightOff(LightTrackmute);
        }

        LCDWrite(track->getLabel(), Bottom);
    }
}
#endif

/**
 * Called when a track is changed (instrument id, muted status...)
 */
void
TranzportClient::trackChanged(const Composition *c,
                              Track* track)
{
    RG_DEBUG << "TranzportClient, CompositionObserver::trackChanged";

    if (device_online) {
        const Track* track2 = c->getTrackById(c->getSelectedTrack());

        // If the changed track is the selected track
        if (track == track2) {
            RG_DEBUG << "TranzportClient, CompositionObserver::trackChanged updating";

            if (track->isArmed()) {
                LightOn(LightTrackrec);
            } else {
                LightOff(LightTrackrec);
            }

            if (track->isMuted()) {
                LightOn(LightTrackmute);
            } else {
                LightOff(LightTrackmute);
            }

            LCDWrite(track->getLabel(), Bottom);
        }
    }
}

void
TranzportClient::loopChanged()
{
#if 0
    RG_DEBUG << "TranzportClient: loopChanged" << t1 << ", " << t2;

    if (device_online) {
        if (t1 == 0  and  t2 == 0) {
            LightOff(LightLoop);
        } else {
            LightOn(LightLoop);
        }
    }
#endif
}

void
TranzportClient::stateUpdate()
{
    if (device_online) {
        LCDWrite("Rosegarden");

        // No longer supported.  Solo has moved to Track.
        //if (m_composition->isSolo()) {
        //    LightOn(LightAnysolo);
        //} else {
            LightOff(LightAnysolo);
        //}

        if (m_composition->getLoopMode() == Composition::LoopOn) {
            LightOn(LightLoop);
        } else {
            LightOff(LightLoop);
        }

        TrackId trackID = m_composition->getSelectedTrack();
        const Track* track = m_composition->getTrackById(trackID);

        if (track->isArmed()) {
            LightOn(LightTrackrec);
        } else {
            LightOff(LightTrackrec);
        }

        if (track->isMuted()) {
            LightOn(LightTrackmute);
        } else {
            LightOff(LightTrackmute);
        }

        LCDWrite(track->getLabel().substr(0,9), Bottom);

        int bar, beat, fraction, remainder;
        m_composition->getMusicalTimeForAbsoluteTime
            (CompositionPosition::getInstance()->get(),
             bar, beat, fraction, remainder);
        std::stringstream ss;
        ss << bar+1 << ":" << beat;
        LCDWrite(ss.str(), Bottom, 10);
    }
}

TranzportClient::~TranzportClient()
{
    delete m_socketReadNotifier;
    delete m_socketWriteNotifier;

    close(m_descriptor);

    RG_DEBUG << "TranzportClient::~TranzportClient: cleaned up ";
}


void TranzportClient::writeCommandQueue()
{
    RG_DEBUG << "TranzportClient: writeCommandQueue";

    if (commands.empty()) {
        m_socketWriteNotifier->setEnabled(false);
        return;
    }

    uint64_t cmd = commands.front();
    int res = ::write(m_descriptor, static_cast<void*>(&cmd), 8);
    m_socketWriteNotifier->setEnabled(false);

    if (res < 0) {
        RG_DEBUG << "TranzportClient::writeCommandQueue: could not write to device, error" << strerror(errno);

        m_socketWriteNotifier->setEnabled(true);
        return;
    } else if (res != 8) {
        RG_DEBUG << "TranzportClient::writeCommandQueue: could not write full data to device";

        commands.pop();
        m_socketWriteNotifier->setEnabled(true);
    }

    commands.pop();

    if (not commands.empty()) {
        m_socketWriteNotifier->setEnabled(true);
    }
}

void
TranzportClient::write(uint64_t buf)
{
    commands.push(buf);
    if (not m_socketWriteNotifier->isEnabled()) {
        RG_DEBUG << "TranzportClient::write Setting the socket write notifier to enabled";
        m_socketWriteNotifier->setEnabled(true);
    }
}

void
TranzportClient::LightOn(Light light)
{
    const uint8_t cmd[8] = {0x00, 0x00, static_cast<uint8_t>(light), 0x01,
                            0x00, 0x00, 0x00, 0x00};
    uint64_t cmd64;
    memcpy(&cmd64, cmd, sizeof(cmd64));
    write(cmd64);
}

void
TranzportClient::LightOff(Light light)
{
    const uint8_t cmd[8] = {0x00, 0x00, static_cast<uint8_t>(light), 0x00,
                            0x00, 0x00, 0x00, 0x00};
    uint64_t cmd64;
    memcpy(&cmd64, cmd, sizeof(cmd64));
    write(cmd64);
}

void
TranzportClient::LCDWrite(const std::string& text,
                          Row row,
                          uint8_t offset)
{
    if (offset >= LCDLength) {
        return;
    }

    std::string str(LCDLength, ' ' );
    str.insert(offset, text.c_str(),
            std::min(text.size(), static_cast<size_t>(LCDLength - offset)));

    uint8_t cmd[8];
    uint8_t cell = row == Top ? 0 : 5;

    for (int i = 0; i < LCDLength;) {
        cmd[0] = 0x00;
        cmd[1] = 0x01;
        cmd[2] = cell++;
        cmd[3] = str[i++];
        cmd[4] = str[i++];
        cmd[5] = str[i++];
        cmd[6] = str[i++];
        cmd[7] = 0x00;
        uint64_t cmd64;
        memcpy(&cmd64, cmd, sizeof(cmd64));
        write(cmd64);
    }
}

void
TranzportClient::readData()
{
    uint8_t buf[8];
    m_previousButtons = m_currentButtons;
    ssize_t val;
    static timeT loop_start_time=0;
    static timeT loop_end_time=0;

    while ((val=read(m_descriptor,buf,8)) == 8) {
        memcpy(&m_currentButtons, buf + 2, sizeof(m_currentButtons));
        const uint8_t status = buf[1];
        const uint8_t datawheel = buf[6];
        uint32_t new_buttons = m_currentButtons ^ m_previousButtons;
        if (status == 0x1) {
            RG_DEBUG << "TranzportClient: device just came online";

            while (not commands.empty()) {
                commands.pop();
            }
            device_online = true;

            m_rgDocument = RosegardenDocument::currentDocument;
            m_composition = &RosegardenDocument::currentDocument->getComposition();
            stateUpdate();
        }

        if (status == 0xff) {
            RG_DEBUG << "TranzportClient: device just went offline";

            device_online = false;
            return;
        }

        // Solo has moved to Track.
#if 0
        if (new_buttons & TrackSolo  and
            m_currentButtons & TrackSolo) {
            if (m_currentButtons & Shift) {
                bool soloflag = m_composition->isSolo();
                emit solo(not soloflag);
            }
        }
#endif

        if (new_buttons & Add  and
            m_currentButtons & Add) {
            if (m_currentButtons & Shift) {
            } else {
                AddMarkerCommand* cmd = new AddMarkerCommand
                    (m_composition,
                     CompositionPosition::getInstance()->get(),
                     "tranzport",
                     "");
                CommandHistory::getInstance()->addCommand(cmd);
            }
        }

        if (new_buttons & Prev  and
            m_currentButtons & Prev) {
            RG_DEBUG << "TranzportClient:: received marker previous";

            if (m_currentButtons & Shift) {
            } else {
                timeT currentTime =
                    CompositionPosition::getInstance()->get();
                Composition::MarkerVector& mc = m_composition->getMarkers();
                timeT closestPrevious = -1;

                for (Composition::MarkerVector::const_iterator it = mc.begin();
                     it != mc.end();
                     ++it) {
                    timeT markerTime = (*it)->getTime();
                    if (markerTime < currentTime  and
                        markerTime > closestPrevious) {
                        closestPrevious = markerTime;
                    }
                }

                if (closestPrevious >= 0) {
                    RG_DEBUG << "Tranzport:: setting position: " << closestPrevious;

                    emit setPosition(closestPrevious);
                }
            }
        }

        if (new_buttons & Next  and
            m_currentButtons & Next)
        {
            RG_DEBUG << "TranzportClient:: received marker next";

            if (m_currentButtons & Shift) {
            } else {
                timeT currentTime =
                    CompositionPosition::getInstance()->get();
                Composition::MarkerVector& mc = m_composition->getMarkers();
                timeT closestNext = std::numeric_limits<long>::max();

                for (Composition::MarkerVector::const_iterator it = mc.begin();
                     it != mc.end();
                     ++it) {
                    timeT markerTime = (*it)->getTime();
                    if (markerTime > currentTime and
                        markerTime < closestNext) {
                        closestNext = markerTime;
                    }
                }

                if (closestNext < std::numeric_limits<long>::max()) {
                    RG_DEBUG << "Tranzport:: setting position: " << closestNext;

                    emit setPosition(closestNext);
                }
            }
        }

        if (new_buttons & Undo  and
            m_currentButtons & Undo) {
            if (m_currentButtons & Shift) {
                emit redo();
            } else {
                emit undo();
            }
        }

        if (new_buttons & Play  and
            m_currentButtons & Play) {
            if (m_currentButtons & Shift) {
            } else {
                emit play();
            }
        }

        if (new_buttons & Stop  and
            m_currentButtons & Stop) {
            if (m_currentButtons & Shift) {
            } else {
                emit stop();
            }
        }

        if (new_buttons & Record  and
            m_currentButtons & Record) {
            if (m_currentButtons & Shift) {
            } else {
                emit record();
            }
        }

        if (new_buttons & Loop  and
            m_currentButtons & Loop) {
            if (m_currentButtons & Shift) {
            } else {
                loop_start_time =
                    CompositionPosition::getInstance()->get();
                loop_end_time = loop_start_time;
            }
        }

        if (new_buttons & Loop  and
            (not (m_currentButtons & Loop))) {
            if (m_currentButtons & Shift) {
            } else {
                if (loop_start_time == loop_end_time) {
                    m_composition->setLoopMode(Composition::LoopOff);
                    emit m_rgDocument->loopChanged();
                }

                loop_start_time = 0;
                loop_end_time = 0;
            }
        }

        if (new_buttons& Rewind  and
            m_currentButtons & Rewind) {
            if (m_currentButtons&Shift) {
                emit rewindToBeginning();
            } else {
                emit rewind();
            }
        }

        if (new_buttons & FastForward  and
            m_currentButtons & FastForward) {
            if (m_currentButtons & Shift) {
                emit fastForwardToEnd();
            } else {
                emit fastForward();
            }
        }

        if (new_buttons & TrackRec  and
            m_currentButtons & TrackRec) {
            if (m_currentButtons & Shift) {
            } else {
                emit trackRecord();
            }
        }

        if (new_buttons & TrackRight  and
            m_currentButtons & TrackRight) {
            if (m_currentButtons & Shift) {
            } else {
                emit trackDown();
            }
        }

        if (new_buttons & TrackLeft  and
            m_currentButtons & TrackLeft) {
            if (m_currentButtons& Shift) {
            } else {
                emit trackUp();
            }
        }

        if (new_buttons & TrackMute  and
            m_currentButtons & TrackMute) {
            if (m_currentButtons & Shift) {
            } else {
                emit trackMute();
            }
        }

        if (datawheel) {
            if (datawheel < 0x7F) {
                if (m_currentButtons & Loop) {

                    loop_end_time += datawheel *
                        m_composition->getDurationForMusicalTime(loop_end_time, 0,1,0,0);

                    m_composition->setLoopMode(Composition::LoopOn);
                    m_composition->setLoopStart(loop_start_time);
                    m_composition->setLoopEnd(loop_end_time);
                    emit m_rgDocument->loopChanged();

                } else if(m_currentButtons & Shift) {
                    timeT here =
                        CompositionPosition::getInstance()->get();
                    here += datawheel * m_composition->getDurationForMusicalTime(here,0,0,1,0);
                    if (here <= m_composition->getEndMarker()) {
                        emit setPosition(here);
                    }
                } else {
                    timeT here =
                        CompositionPosition::getInstance()->get();
                    here += datawheel * m_composition->getDurationForMusicalTime(here,0,1,0,0);
                    if (here <= m_composition->getEndMarker()) {
                        emit setPosition(here);
                    }
                }
            } else {
#define DATAWHEEL_VALUE (1 + (0xFF - (datawheel)))
                if (m_currentButtons & Loop) {
                    loop_end_time -= (1 + (0xFF - datawheel)) *
                        RosegardenDocument::currentDocument->getComposition().getDurationForMusicalTime(loop_end_time, 0,1,0,0);
                    m_composition->setLoopMode(Composition::LoopOn);
                    m_composition->setLoopStart(loop_start_time);
                    m_composition->setLoopEnd(loop_end_time);
                    emit m_rgDocument->loopChanged();
                }

                if (m_currentButtons & Shift) {
                    timeT here =
                        CompositionPosition::getInstance()->get();
                    here -= DATAWHEEL_VALUE *  m_composition->getDurationForMusicalTime(here,0,0,1,0);
                    if (here >= m_composition->getStartMarker()) {
                        emit setPosition(here);
                    }
                } else {
                    timeT here =
                        CompositionPosition::getInstance()->get();
                    here -= DATAWHEEL_VALUE *  m_composition->getDurationForMusicalTime(here,0,1,0,0);
                    if (here >= m_composition->getStartMarker()) {
                        emit setPosition(here);
                    }
                }
#undef DATAWHEEL_VALUE
            }
        }

        m_previousButtons = m_currentButtons;
    }

    if (val == -1) {
        if (errno == EAGAIN) {
            return;
        } else {
            RG_DEBUG << "TranzportClient::readData: error " << strerror(errno);
        }
    } else {
        RG_DEBUG << "TranzportClient::readData: partial read of length " << val;
        RG_DEBUG << "TranzportClient::readData: this should not happen " << val;
    }
}

}
