function nl2_track_editor(csvFile)
%NL2_TRACK_EDITOR  Interactive viewer/editor for NoLimits2 CSV track exports.
%
%   nl2_track_editor()            - prompts you to pick a CSV file
%   nl2_track_editor('file.csv')  - loads the given file directly
%
% WHAT THIS DOES
%   NoLimits2's "Track Data Export" gives you one centreline (PosX/Y/Z)
%   plus an orientation at each station (Left, Up vectors). That centre
%   point is NOT a rail and NOT your build spline - it's just the path
%   NL2 walked the camera/car along. This tool works out, at every
%   station:
%       - LEFT rail centreline   = centre + (gauge/2) * Left
%       - RIGHT rail centreline  = centre - (gauge/2) * Left
%       - SPLINE / backbone      = centre +/- (spline offset) * Up
%   using your rail gauge and spline-offset-from-rails as live,
%   editable parameters (defaults: 26 mm gauge, 15 mm spline offset -
%   change these to your real numbers).
%
% A NOTE ON NL2's EXPORTED VECTORS
%   In this export format the raw "Left" and "Up" columns are not
%   always exactly perpendicular to each other (there's a small,
%   fairly constant residual, likely a heartline/camera artifact in
%   how NL2 writes this file). "Up" is the vector that matters
%   physically - it's what actually encodes the bank/roll a rider
%   feels - so by default this tool trusts Up, works out the true
%   direction of travel (Front) from the position spline itself, and
%   DERIVES a Left that is exactly perpendicular to both. This
%   guarantees the two rails always sit in a true cross-sectional
%   plane (no twist). If you'd rather trust NL2's raw Left column
%   as-is, flip the "Orientation frame" dropdown and compare - both
%   are plotted the same way so you can sanity-check against the NL2
%   in-game preview.
%
% REQUIREMENTS
%   MATLAB R2019b or newer (uses uifigure/uigridlayout/uitable). No
%   extra toolboxes needed.
%
% CROSSTIES
%   Set "Crosstie spacing (mm)" (default 25) and the tool resamples the
%   whole track at even arc-length intervals along the centreline, and
%   works out the left-rail, right-rail and spline point for each
%   station - the 3 mate points for one crosstie. These are shown as
%   grey tie lines in the preview. Tick "include in export" to have
%   them written out alongside the main curves: as CSV/TXT they land in
%   crosstie_left/_right/_spline files (plus one combined
%   _crosstie_all table you can eyeball the spacing in); as SLDCRV each
%   one imports into SolidWorks as a curve whose vertices ARE your
%   25 mm-spaced mate points - select a vertex on the left-rail curve,
%   one on the right-rail curve and one on the spline curve to mate
%   each crosstie part in place.
%
% EXPORT
%   Tick whichever formats you need next to "Export formats:" and hit
%   "Export...". All coordinates are written in mm.
%     CSV     - center/left_rail/right_rail/spline, each with a header
%               and point number, plus one combined _all.csv.
%     TXT     - the same data, tab-separated, plus a combined _all.txt.
%     SLDCRV  - one file per curve (center/left_rail/right_rail/spline),
%               plain "X Y Z" per line with no header - the format
%               SolidWorks' Curve Through XYZ Points needs. Set your
%               SolidWorks document units to mm before importing, since
%               .sldcrv files carry no unit information themselves.
%
%   Frankie Pang Studios helper script.

    existing = findall(0,'Type','figure','Tag','NL2TrackEditor');
    if ~isempty(existing)
        close(existing);
    end

    %% ---------- shared state ----------
    data = table();                 % working point table (No, PosX..UpZ)
    outCenter = []; outLeftRail = []; outRightRail = []; outSpline = [];

    %% ---------- figure & layout ----------
    fig = uifigure('Name','NL2 Track Editor - Frankie Pang Studios', ...
        'Position',[80 60 1420 820],'Tag','NL2TrackEditor');
    gl = uigridlayout(fig,[1 2]);
    gl.ColumnWidth = {440,'1x'};

    leftPanel  = uipanel(gl,'Title','Track points (editable)');
    rightPanel = uipanel(gl,'Title','3D preview');

    lgl = uigridlayout(leftPanel,[9 2]);
    lgl.RowHeight = {30,30,30,30,30,30,30,'1x',34};

    l1 = uilabel(lgl,'Text','CSV position units:'); l1.Layout.Row=1; l1.Layout.Column=1;
    ddUnits = uidropdown(lgl,'Items',{'metres (m)','millimetres (mm)'},'Value','metres (m)', ...
        'ValueChangedFcn',@(s,e) recomputeAndPlot());
    ddUnits.Layout.Row=1; ddUnits.Layout.Column=2;

    l2 = uilabel(lgl,'Text','Model scale factor:'); l2.Layout.Row=2; l2.Layout.Column=1;
    efScale = uieditfield(lgl,'numeric','Value',1,'Limits',[0 Inf],'LowerLimitInclusive','off', ...
        'ValueChangedFcn',@(s,e) recomputeAndPlot());
    efScale.Layout.Row=2; efScale.Layout.Column=2;

    l3 = uilabel(lgl,'Text','Rail gauge (mm):'); l3.Layout.Row=3; l3.Layout.Column=1;
    efGauge = uieditfield(lgl,'numeric','Value',26,'Limits',[0 Inf], ...
        'ValueChangedFcn',@(s,e) recomputeAndPlot());
    efGauge.Layout.Row=3; efGauge.Layout.Column=2;

    l4 = uilabel(lgl,'Text','Spline offset (mm):'); l4.Layout.Row=4; l4.Layout.Column=1;
    efSpline = uieditfield(lgl,'numeric','Value',15,'Limits',[0 Inf], ...
        'ValueChangedFcn',@(s,e) recomputeAndPlot());
    efSpline.Layout.Row=4; efSpline.Layout.Column=2;

    l5 = uilabel(lgl,'Text','Spline side:'); l5.Layout.Row=5; l5.Layout.Column=1;
    ddSplineSide = uidropdown(lgl,'Items',{'Below rails (-Up)','Above rails (+Up)'}, ...
        'Value','Below rails (-Up)','ValueChangedFcn',@(s,e) recomputeAndPlot());
    ddSplineSide.Layout.Row=5; ddSplineSide.Layout.Column=2;

    l6 = uilabel(lgl,'Text','Orientation frame:'); l6.Layout.Row=6; l6.Layout.Column=1;
    ddFrame = uidropdown(lgl,'Items',{'Derived (Front x Up) - recommended','Raw NL2 Left column'}, ...
        'Value','Derived (Front x Up) - recommended','ValueChangedFcn',@(s,e) recomputeAndPlot());
    ddFrame.Layout.Row=6; ddFrame.Layout.Column=2;

    fmtRow = uigridlayout(lgl,[1 4],'Padding',[0 0 0 0],'ColumnWidth',{110,'1x','1x','1x'});
    fmtRow.Layout.Row = 7; fmtRow.Layout.Column = [1 2];
    uilabel(fmtRow,'Text','Export formats:');
    cbCSV    = uicheckbox(fmtRow,'Text','CSV','Value',true);
    cbTXT    = uicheckbox(fmtRow,'Text','TXT','Value',false);
    cbSLDCRV = uicheckbox(fmtRow,'Text','SLDCRV','Value',false);

    expected = {'No','PosX','PosY','PosZ','FrontX','FrontY','FrontZ', ...
                'LeftX','LeftY','LeftZ','UpX','UpY','UpZ'};
    tbl = uitable(lgl,'ColumnEditable',[false true(1,12)],'ColumnName',expected);
    tbl.Layout.Row = 8; tbl.Layout.Column = [1 2];
    tbl.CellEditCallback = @(s,e) onTableEdited();

    btnRow = uigridlayout(lgl,[1 4],'Padding',[0 0 0 0]);
    btnRow.Layout.Row = 9; btnRow.Layout.Column = [1 2];
    uibutton(btnRow,'Text','Load CSV...','ButtonPushedFcn',@(s,e) onLoad());
    uibutton(btnRow,'Text','Add point','ButtonPushedFcn',@(s,e) onAddPoint());
    uibutton(btnRow,'Text','Delete selected','ButtonPushedFcn',@(s,e) onDeletePoint());
    uibutton(btnRow,'Text','Export...','ButtonPushedFcn',@(s,e) onExport());

    rgl = uigridlayout(rightPanel,[1 1]);
    ax = uiaxes(rgl);
    view(ax,3); grid(ax,'on'); axis(ax,'equal'); rotate3d(ax,'on');
    xlabel(ax,'X (mm)'); ylabel(ax,'Y (mm)'); zlabel(ax,'Z (mm)');

    %% ---------- initial load ----------
    if nargin >= 1 && ~isempty(csvFile)
        loadFile(csvFile);
    end

    %% ================= callbacks =================
    function onLoad()
        [f,p] = uigetfile({'*.csv'},'Select NoLimits2 track CSV export');
        if isequal(f,0), return; end
        loadFile(fullfile(p,f));
    end

    function loadFile(fname)
        try
            raw = readtable(fname);
        catch err
            uialert(fig, sprintf('Could not read file:\n%s', err.message), 'Load error');
            return;
        end
        if width(raw) < 13
            uialert(fig,'CSV does not have the 13 expected NL2 export columns.','Load error');
            return;
        end
        raw.Properties.VariableNames{1} = 'No';   % NL2 exports "No." which MATLAB mangles
        raw.Properties.VariableNames(2:13) = expected(2:13);
        data = raw(:,expected);
        tbl.Data = data;
        recomputeAndPlot();
    end

    function onTableEdited()
        data = tbl.Data;
        recomputeAndPlot();
    end

    function onAddPoint()
        if isempty(data), uialert(fig,'Load a track first.','No data'); return; end
        newRow = data(end,:);
        newRow.No = data.No(end) + 1;
        data = [data; newRow];
        tbl.Data = data;
        recomputeAndPlot();
    end

    function onDeletePoint()
        sel = tbl.Selection;
        if isempty(sel), uialert(fig,'Click a cell in a row first.','Nothing selected'); return; end
        rowsToDelete = unique(sel(:,1));
        data(rowsToDelete,:) = [];
        tbl.Data = data;
        recomputeAndPlot();
    end

    function onExport()
        if isempty(outCenter), uialert(fig,'Nothing to export yet - load a track first.','No data'); return; end
        if ~(cbCSV.Value || cbTXT.Value || cbSLDCRV.Value)
            uialert(fig,'Tick at least one export format (CSV / TXT / SLDCRV).','No format selected');
            return;
        end
        [f,p] = uiputfile('*.*','Base name for exported files (extension is added automatically)','nl2_track_export');
        if isequal(f,0), return; end
        [~,base,~] = fileparts(fullfile(p,f));

        curves = {outCenter,'center'; outLeftRail,'left_rail'; outRightRail,'right_rail'; outSpline,'spline'};
        nWritten = 0;
        for k = 1:size(curves,1)
            XYZ = curves{k,1};
            tag = curves{k,2};
            if cbCSV.Value
                writeXYZCsv(fullfile(p,[base '_' tag '.csv']), XYZ);
                nWritten = nWritten + 1;
            end
            if cbTXT.Value
                writeXYZTxt(fullfile(p,[base '_' tag '.txt']), XYZ);
                nWritten = nWritten + 1;
            end
            if cbSLDCRV.Value
                writeSLDCRV(fullfile(p,[base '_' tag '.sldcrv']), XYZ);
                nWritten = nWritten + 1;
            end
        end

        % combined multi-curve file - only makes sense for CSV/TXT
        % (SLDCRV must contain exactly one X Y Z curve per file, so it's
        % skipped here - each curve above already got its own .sldcrv)
        if cbCSV.Value || cbTXT.Value
            Tall = table(data.No, outCenter(:,1),outCenter(:,2),outCenter(:,3), ...
                outLeftRail(:,1),outLeftRail(:,2),outLeftRail(:,3), ...
                outRightRail(:,1),outRightRail(:,2),outRightRail(:,3), ...
                outSpline(:,1),outSpline(:,2),outSpline(:,3), ...
                'VariableNames', {'No','CenterX_mm','CenterY_mm','CenterZ_mm', ...
                'LeftRailX_mm','LeftRailY_mm','LeftRailZ_mm', ...
                'RightRailX_mm','RightRailY_mm','RightRailZ_mm', ...
                'SplineX_mm','SplineY_mm','SplineZ_mm'});
            if cbCSV.Value
                writetable(Tall, fullfile(p,[base '_all.csv']));
                nWritten = nWritten + 1;
            end
            if cbTXT.Value
                writetable(Tall, fullfile(p,[base '_all.txt']), 'FileType','text', 'Delimiter','\t');
                nWritten = nWritten + 1;
            end
        end

        msg = sprintf('Exported %d file(s) to:\n%s', nWritten, p);
        if cbSLDCRV.Value
            msg = [msg sprintf('\n\nNote: .sldcrv files carry no unit metadata - set your SolidWorks document units to millimetres before importing the curve.')];
        end
        uialert(fig, msg, 'Export complete','Icon','success');
    end

    function writeXYZCsv(fname, XYZ)
        % Comma-separated, with point number and header row.
        T = table(data.No, XYZ(:,1), XYZ(:,2), XYZ(:,3), ...
            'VariableNames', {'No','X_mm','Y_mm','Z_mm'});
        writetable(T, fname);
    end

    function writeXYZTxt(fname, XYZ)
        % Tab-separated, with point number and a commented header row.
        fid = fopen(fname,'w');
        if fid < 0
            uialert(fig, sprintf('Could not write:\n%s', fname), 'Export error');
            return;
        end
        fprintf(fid, '%% No\tX_mm\tY_mm\tZ_mm\n');
        for i = 1:size(XYZ,1)
            fprintf(fid, '%d\t%.6f\t%.6f\t%.6f\n', data.No(i), XYZ(i,1), XYZ(i,2), XYZ(i,3));
        end
        fclose(fid);
    end

    function writeSLDCRV(fname, XYZ)
        % SolidWorks curve-through-XYZ-points format: plain space-
        % separated X Y Z, one point per line, no header, no index
        % column - SolidWorks will reject anything extra in the file.
        fid = fopen(fname,'w');
        if fid < 0
            uialert(fig, sprintf('Could not write:\n%s', fname), 'Export error');
            return;
        end
        for i = 1:size(XYZ,1)
            fprintf(fid, '%.6f %.6f %.6f\n', XYZ(i,1), XYZ(i,2), XYZ(i,3));
        end
        fclose(fid);
    end

    %% ================= core computation =================
    function recomputeAndPlot()
        if isempty(data) || height(data) < 2
            return;
        end

        if strcmp(ddUnits.Value,'metres (m)')
            posUnitsScale = 1000;   % m -> mm
        else
            posUnitsScale = 1;      % already mm
        end
        scaleFactor = efScale.Value;      % extra shrink/grow for scale models
        gauge       = efGauge.Value;      % mm
        splineOff   = efSpline.Value;     % mm
        if strcmp(ddSplineSide.Value,'Below rails (-Up)')
            splineSign = -1;
            sideStr = 'below';
        else
            splineSign = 1;
            sideStr = 'above';
        end
        useRawLeft = strcmp(ddFrame.Value,'Raw NL2 Left column');

        P    = [data.PosX, data.PosY, data.PosZ] * (posUnitsScale * scaleFactor);  % n x 3, mm
        Lraw = [data.LeftX, data.LeftY, data.LeftZ];
        Uraw = [data.UpX,   data.UpY,   data.UpZ];
        n = size(P,1);

        % Front: tangent to the position spline (central differences)
        F = zeros(n,3);
        F(1,:)       = P(2,:)   - P(1,:);
        F(end,:)     = P(end,:) - P(end-1,:);
        F(2:end-1,:) = P(3:end,:) - P(1:end-2,:);
        F = F ./ vecnorm(F,2,2);

        % Up: NL2's Up vector, forced exactly perpendicular to Front
        Uraw = Uraw ./ vecnorm(Uraw,2,2);
        U = Uraw - sum(Uraw.*F,2).*F;
        U = U ./ vecnorm(U,2,2);

        % Left: derived cleanly from Front x Up (guaranteed orthonormal)
        Lderived = cross(U, F, 2);
        Lderived = Lderived ./ vecnorm(Lderived,2,2);
        flipSign = sign(sum(Lderived.*Lraw,2));
        flipSign(flipSign==0) = 1;
        Lderived = Lderived .* flipSign;   % match NL2's raw Left sense

        if useRawLeft
            Lused = Lraw ./ vecnorm(Lraw,2,2);
        else
            Lused = Lderived;
        end

        halfGauge = gauge/2;
        leftRail  = P + halfGauge .* Lused;
        rightRail = P - halfGauge .* Lused;
        spline    = P + (splineSign * splineOff) .* U;

        outCenter = P; outLeftRail = leftRail; outRightRail = rightRail; outSpline = spline;

        cla(ax); hold(ax,'on');
        plot3(ax, P(:,1),P(:,2),P(:,3), ':', 'Color',[0.5 0.5 0.5], 'LineWidth',1, ...
            'DisplayName','NL2 centre (as exported)');
        plot3(ax, leftRail(:,1),leftRail(:,2),leftRail(:,3), '-', 'Color',[0.85 0.1 0.1], ...
            'LineWidth',2, 'DisplayName','Left rail');
        plot3(ax, rightRail(:,1),rightRail(:,2),rightRail(:,3), '-', 'Color',[0.1 0.35 0.85], ...
            'LineWidth',2, 'DisplayName','Right rail');
        plot3(ax, spline(:,1),spline(:,2),spline(:,3), '-', 'Color',[0.15 0.65 0.15], ...
            'LineWidth',2, 'DisplayName','Spline / backbone');
        step = max(1, round(n/20));
        quiver3(ax, P(1:step:end,1),P(1:step:end,2),P(1:step:end,3), ...
            U(1:step:end,1),U(1:step:end,2),U(1:step:end,3), 0.4, ...
            'Color',[0.9 0.6 0], 'LineWidth',1, 'DisplayName','Up (bank) vector');
        hold(ax,'off');
        axis(ax,'equal'); grid(ax,'on');
        xlabel(ax,'X (mm)'); ylabel(ax,'Y (mm)'); zlabel(ax,'Z (mm)');
        legend(ax,'Location','best');
        if useRawLeft, frameStr = 'raw NL2 Left'; else, frameStr = 'derived'; end
        title(ax, sprintf('Gauge %.1f mm | Spline offset %.1f mm (%s rails) | frame: %s', ...
            gauge, splineOff, sideStr, frameStr));
    end

end
