#! /usr/bin/python3

r'''###############################################################################
###################################################################################
#
#
#	MIDI Splitter Python Module
#	Version 1.0
#
#	Project Los Angeles
#
#	Tegridy Code 2026
#
#   https://github.com/Tegridy-Code/Project-Los-Angeles
#
#
###################################################################################
###################################################################################
#
#   Copyright 2026 Project Los Angeles / Tegridy Code
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#
###################################################################################
'''

###################################################################################
###################################################################################

print('=' * 70)
print('Loading midisplitter Python module...')
print('Please wait...')
print('=' * 70)

__version__ = '1.0.0'

print('midisplitter module version', __version__)
print('=' * 70)

###################################################################################
###################################################################################

import os

import copy

from .constants import Number2patch, Number2drumkit

from .MIDI import midi2score, score2midi

###################################################################################
###################################################################################

instrument_name = lambda s: ''.join(c for c in s.lower().replace('(', ' ').replace(')', ' ').strip() if c.isalpha() or c==' ').strip().replace('  ', '_').replace(' ', '_')

###################################################################################

special_events = ['key_after_touch', 'control_change', 'channel_after_touch', 'pitch_wheel_change']

###################################################################################

def split_midi(midi_file, output_dir=None):
    
    """
    Split a MIDI file into per‑instrument stems by analyzing channels, patch
    assignments, and event types, then write each stem as an individual MIDI file.

    This function loads a MIDI file using `midi2score`, normalizes channel and
    value ranges for `note` and `patch_change` events, and reconstructs a set of
    independent instrument‑specific scores. Each output file contains all note
    events for a single (track_name, channel, program) combination, plus all
    non-note global events preserved for timing and musical correctness.

    Core behavior
    -------------
    1. **Normalization of raw events**
       - Ensures MIDI channels are in `[0, 15]` and velocities/pitches in
         `[0, 127]`.
       - Normalizes `patch_change` events and maps channel 9 (drums) to the
         extended 128–255 drum‑kit program range.

    2. **Track‑level processing**
       - Extracts the track name if present; otherwise assigns `"Track #i"`.
       - Builds a per‑channel patch table for the track.
       - Collects all non-note, non-special events into a global pool
         (`all_other_events`) that will be injected into every output stem.

    3. **Instrument/voice extraction**
       - For each channel that contains notes, gathers:
         - All `note` events for that channel.
         - All `special_events` whose channel matches.
         - All other non-note events (meta, tempo, markers, etc.).
       - Produces a list of `(track_name, program_number, score_events)` tuples.

    4. **Stem construction**
       - Merges channel‑specific events with the global event pool.
       - Sorts all events by absolute time.
       - Rewrites channels:
         - Melodic instruments → channel 0.
         - Drum kits → channel 9.
       - Prepends a minimal header containing:
         - `track_name`
         - A `patch_change` for the resolved program (melodic or drum‑kit).

    5. **Output**
       - Converts each stem back to MIDI using `score2midi`.
       - Names files using zero‑padded index, instrument name, and program
         number, e.g. `"003_AcousticGrandPiano_0.mid"`.
       - Writes files into `output_dir` if provided, creating it if needed.

    Parameters
    ----------
    midi_file : str or Path
        Path to the input MIDI file to be split.
    output_dir : str or Path, optional
        Directory where output stems will be written. If omitted, files are
        written to the current working directory.

    Returns
    -------
    None
        The function writes one MIDI file per detected instrument stem and
        performs no in‑memory return.

    Notes
    -----
    - Drum channels (MIDI channel 9) are mapped to program numbers 128–255 to
      distinguish drum kits from melodic patches.
    - All non-note global events (meta, markers, tempo, etc.) are duplicated
      into every stem to preserve timing and structure.
    - The function assumes `special_events` is defined in the surrounding scope
      and contains event types that should be treated as channel‑specific.
    """
    
    raw_score = midi2score(open(midi_file, 'rb').read())
    
    # ----------------------------------------------------------------------------

    midi_ticks = raw_score[0]
    
    all_tracks = raw_score[1:]

    all_clean_tracks = []

    for track in all_tracks:
        new_track = copy.deepcopy(track)
        for e in new_track:
        
          if e[0] == 'note':
              e[3] = e[3] % 16
              e[4] = e[4] % 128
              e[5] = e[5] % 128
        
          if e[0] == 'patch_change':
              e[2] = e[2] % 16
              e[3] = e[3] % 128

        all_clean_tracks.append(new_track)
        
    # ----------------------------------------------------------------------------

    all_scores = []
    all_other_events = []

    for i, track in enumerate(all_clean_tracks):
        
        track_names = [e for e in track if e[0] == 'track_name']
        track_name = track_names[0][2] if track_names else 'Track #' + str(i)
        
        track_patches = [0] * 16

        for e in track:
            if e[0] == 'patch_change':
                track_patches[e[2]] = e[3] if e[2] != 9 else e[3]+128

        other_events = [e for e in track if e[0] != 'note' and e[0] not in special_events]
        all_other_events.extend(other_events)

        track_chans = sorted(set([e[3] for e in track if e[0] == 'note']))
                
        for cha in track_chans:
            score = []

            for e in track:
                if e[0] == 'note' and e[3] == cha:
                    score.append(e)
                    
                if e[0] in special_events and e[2] == cha:
                    score.append(e)

                if e[0] != 'note' and e[0] not in special_events:
                    score.append(e)
   
            if score:
                all_scores.append([track_name, track_patches[cha], score])
    
    # ----------------------------------------------------------------------------
    
    if all_scores:

        for i, (tname, tpat, score) in enumerate(all_scores):

            new_score = copy.deepcopy(score)

            new_score.extend(all_other_events)
            new_score.sort(key=lambda x: x[1])
            
            if tpat < 128:
                for e in new_score:
                    if e[0] == 'note':
                        e[3] = 0

                    if e[0] in special_events:
                        e[2] = 0

            output_header = [['track_name', 0, tname]]
            
            if tpat < 128:
                output_header.append(['patch_change', 0, 0, tpat])
                
            else:
                output_header.append(['patch_change', 0, 9, tpat-128])

            output = [midi_ticks, output_header + new_score]
            
            midi_data = score2midi(output)

            if tpat < 128:
                output_file_name = str(i).zfill(3) + '_' + instrument_name(Number2patch[tpat]) + '_' + str(tpat)

            else:
                dpat = tpat - 128

                if dpat in Number2drumkit:
                    output_file_name = str(i).zfill(3) + '_' + instrument_name(Number2drumkit[dpat]) + '_' + str(dpat)

                else:
                    output_file_name = str(i).zfill(3) + '_' + 'custom_kit' + '_' + str(dpat)
                    
            output_file_name += '.mid'
                    
            if output_dir is not None:
                os.makedirs(output_dir, exist_ok=True)
                
                output_file_name = os.path.join(output_dir, output_file_name)
            
            with open(output_file_name, 'wb') as midi_file:
                midi_file.write(midi_data)
                midi_file.close()

###################################################################################

print('Module is loaded!')
print('Enjoy! :)')
print('=' * 70)

###################################################################################
# This is the end of the midisplitter Python module
###################################################################################