mod allocator;
mod args;
mod audio_chunk;
mod audio_data;
mod beep;
mod chunk_factory;
mod column;
mod connection_manager;
mod controllers;
mod looper_error;
mod metronome;
mod midi;
mod notification_handler;
mod osc_server;
mod persistence_manager;
mod post_record_handler;
mod process_handler;
mod state;
mod tap_tempo;
mod track;
mod track_matrix;

use std::sync::Arc;

use allocator::Allocator;
use args::Args;
use audio_chunk::AudioChunk;
use audio_data::AudioData;
use beep::generate_beep;
use chunk_factory::ChunkFactory;
use column::Column;
use connection_manager::ConnectionManager;
use controllers::ColumnControllers;
use controllers::MatrixControllers;
use controllers::TrackControllers;
use looper_error::LooperError;
use looper_error::Result;
use metronome::BufferTiming;
use metronome::Metronome;
use notification_handler::JackNotification;
use notification_handler::NotificationHandler;
use osc_server::Osc;
use osc_server::OscController;
use persistence_manager::PersistenceManager;
use persistence_manager::PersistenceManagerController;
use post_record_handler::PostRecordController;
use post_record_handler::PostRecordHandler;
use post_record_handler::PostRecordResponse;
use process_handler::ProcessHandler;
use state::State;
use tap_tempo::TapTempo;
use track::Track;
use track::TrackState;
use track_matrix::TrackMatrix;

const COLS: usize = 5;
const ROWS: usize = 5;

pub struct JackPorts {
    pub audio_in: jack::Port<jack::AudioIn>,
    pub audio_out: jack::Port<jack::AudioOut>,
    pub click_track_out: jack::Port<jack::AudioOut>,
    pub midi_in: jack::Port<jack::MidiIn>,
    pub midi_out: jack::Port<jack::MidiOut>,
}

#[tokio::main]
async fn main() {
    let args = Args::new().expect("Could not parse arguments");
    simple_logger::SimpleLogger::new()
        .init()
        .expect("Could not initialize logger");

    let (jack_client, ports) = setup_jack();

    let mut allocator = Allocator::spawn(jack_client.sample_rate(), 3);

    let beep_samples = generate_beep(jack_client.sample_rate() as u32, &mut allocator)
        .expect("Could not generate beep samples");

    let notification_handler = NotificationHandler::new();
    let mut notification_channel = notification_handler.subscribe();

    let (mut persistence_manager, persistence_controller) =
        PersistenceManager::new(&args, notification_handler.subscribe());
    let state = persistence_manager.state();

    // Calculate tempo from state and jack client
    let tempo_bpm =
        (jack_client.sample_rate() as f32 * 60.0) / state.borrow().metronome.frames_per_beat as f32;

    let (mut osc, osc_controller) = Osc::<COLS, ROWS>::new(&args.socket, tempo_bpm)
        .await
        .expect("Could not create OSC server");

    // Create post-record handler and get controller for ProcessHandler
    let (mut post_record_handler, post_record_controller) =
        PostRecordHandler::new(&args).expect("Could not create post-record handler");

    let process_handler = ProcessHandler::<_, COLS, ROWS>::new(
        &jack_client,
        ports,
        allocator,
        beep_samples,
        &state.borrow(),
        post_record_controller,
        osc_controller,
        persistence_controller,
    )
    .expect("Could not create process handler");

    let mut connection_manager = ConnectionManager::new(
        persistence_manager.state(),
        notification_handler.subscribe(),
        jack_client.name().to_string(),
    )
    .expect("Could not create connection manager");

    let _active_client = jack_client
        .activate_async(notification_handler, process_handler)
        .expect("Could not activate Jack");

    loop {
        tokio::select! {
            result = osc.run() => {
                if let Err(e) = result {
                    log::error!("OSC task failed: {}", e);
                }
                break;
            }
            notification = notification_channel.recv() => {
                if let Ok(JackNotification::Shutdown {reason, status}) = notification {
                    log::error!("Jack shutdown: {reason} {status:?}");
                    break;
                }
            }
            result = persistence_manager.run() => {
                if let Err(e) = result {
                    log::error!("StateManager task failed: {}", e);
                }
                break;
            }
            result = connection_manager.run() => {
                if let Err(e) = result {
                    log::error!("ConnectionManager task failed: {}", e);
                }
                break;
            }
            result = post_record_handler.run() => {
                if let Err(e) = result {
                    log::error!("PostRecordHandler task failed: {}", e);
                }
                break;
            }
            _ = tokio::signal::ctrl_c() => {
                log::info!("Stopping");
                break;
            }
        }
    }
}

fn setup_jack() -> (jack::Client, JackPorts) {
    let (jack_client, jack_status) =
        jack::Client::new("looper", jack::ClientOptions::NO_START_SERVER)
            .expect("Could not create Jack client");
    if !jack_status.is_empty() {
        panic!("Could not start jack client: {jack_status:?}");
    }
    let audio_in = jack_client
        .register_port("audio_in", jack::AudioIn::default())
        .expect("Could not create audio_in port");
    let audio_out = jack_client
        .register_port("audio_out", jack::AudioOut::default())
        .expect("Could not create audio_out port");
    let click_track_out = jack_client
        .register_port("click_track", jack::AudioOut::default())
        .expect("Could not create click_track_out port");
    let midi_in = jack_client
        .register_port("midi_in", jack::MidiIn::default())
        .expect("Could not create midi_in port");
    let midi_out = jack_client
        .register_port("midi_out", jack::MidiOut::default())
        .expect("Could not create midi_out port");

    let ports = JackPorts {
        audio_in,
        audio_out,
        click_track_out,
        midi_in,
        midi_out,
    };

    (jack_client, ports)
}
