classdef F3DCompCore < handle
% 3D Foundation Component Core

%   Copyright 1998-2021 HUMUSOFT s.r.o.
%   ^..^

    properties
        % Public
        WorldName
        ActorName
        InstanceType   % 1
        CreateActor
        PreallocatedInstances
        
        FileAtRestart
        ParentActorName
        SourceFile
        SourceScale

        ScriptAtRestart
        InitScript

        InputPorts
        OutputPorts
        Input
        Output

        ShowStatus
        LastError
        ShouldDoPreview

        % Internal
        World
        Actor

        LoadedInstances
        Instance
    end

    methods
        % Constructor
        function obj = F3DCompCore
            obj.ResetInstances;
        end

        % Reset
        function ResetInstances(obj)
            obj.LoadedInstances = false(0);
            obj.Instance = [];
            obj.Instance(1).Input(1).Actor = [];
            obj.Instance(1).Output(1).Actor = [];
            obj.LastError = 0;
        end

        function InstanceReady = ManageInstanceAtRestart(obj, InstID)
            % Resize the LoadInstance variable when trying to access an unknown index
            if abs(InstID) > numel(obj.LoadedInstances)
                obj.LoadedInstances(abs(InstID)) = false;
            end

            % Reload the instance
            if (InstID > 0) && obj.CreateActor
                obj.CreateActorInstance(InstID, obj.FileAtRestart);   % reset the Actor if a file should be reloaded

                if obj.FileAtRestart
                    obj.InitWithFile(InstID);
                end
                
                if obj.ScriptAtRestart
                    obj.ApplyScript(InstID);
                end
                
                OK = obj.AssignPorts(InstID);

                obj.LoadedInstances(InstID) = OK;
                InstanceReady = obj.LoadedInstances(InstID);
                obj.PublishError(1 - OK);
            else
                InstanceReady = false;
            end
        end

        function InstanceReady = ManageInstance(obj, InstID)
            % Resize the LoadInstance variable when trying to access an unknown index
            if abs(InstID) > numel(obj.LoadedInstances)
                obj.LoadedInstances(abs(InstID)) = false;
            end

            if (InstID ~= 0) && ~isnan(InstID)
                if InstID > 0
                    % Load a new instance
                    if ~obj.LoadedInstances(InstID)
                        if obj.CreateActor
                            OK = obj.CreateActorInstance(InstID, false) && ...
                                obj.InitWithFile(InstID) && ...
                                obj.ApplyScript(InstID);
                        else
                            OK = obj.InitAsRef(InstID);
                        end
                        
                        if OK
                            OK = obj.AssignPorts(InstID);
                        end
                        
                        obj.LoadedInstances(InstID) = OK;
                    end
                else
                    % Delete an existing instance
                    InstID = -InstID;
                    if obj.InstanceType < 3
                        if obj.LoadedInstances(InstID)
                            obj.Instance(InstID).BaseActor.remove;
                            obj.LoadedInstances(InstID) = false;
                        end
                    else
                        Actor = obj.World.Root.findByID(InstID);
                        if ~isempty(Actor)
                            Actor.remove;
                            obj.LoadedInstances(InstID) = false;
                        end
                    end
                end
                InstanceReady = obj.LoadedInstances(InstID);
            else
                % Unknown instance or ActorID
                InstanceReady = false;
            end
        end

        function OK = CreateActorInstance(obj, InstID, ResetInstance)
            % Find parent actor
            if strcmpi(obj.ParentActorName, 'Root')
                ParentActor = obj.World.Root;
            else
                ParentActor = F3D.Aux.Trivia.getRefActor(obj.World.Root, obj.ParentActorName);
                if isempty(ParentActor)
                    OK = false;
                    return
                end
            end

            % Create instance actor
            if obj.InstanceType == 2
                actorName = [obj.ActorName num2str(InstID)];
            else
                actorName = obj.ActorName;
            end
            if ResetInstance && isfield(ParentActor.Children, actorName)
                ParentActor.Children.(actorName).remove;
            end
            obj.Actor = ParentActor.addChild(actorName);
            obj.Instance(InstID).BaseActor = obj.Actor;
            
            OK = true;
        end

        %
        function OK = InitWithFile(obj, InstID)
            % Load file
            if ~isempty(obj.SourceFile)
                [~, name, ext] = fileparts(strtrim(obj.SourceFile));
                if strcmpi(ext, '.m')
                    switch nargin(name)
                        case 1
                            feval(name, obj.Actor);
                        case 2
                            feval(name, obj.Actor, obj.World);
                        case 3
                            feval(name, obj.Actor, obj.World, InstID);
                        otherwise
                            error(message('sl3d:f3d:simnotenoughparameters', name));
                    end
                else
                    obj.Actor.load(obj.SourceFile, obj.SourceScale);
                end
            end
            OK = true;
        end

        function OK = InitAsRef(obj, InstID)
            % Find the main reference
            switch obj.InstanceType
                case 1
                    obj.Actor = F3D.Aux.Trivia.getRefActor(obj.World.Root, obj.ActorName);
                case 2
                    obj.Actor = F3D.Aux.Trivia.getRefActor(obj.World.Root, [obj.ActorName num2str(InstID)]);
                case 3
                    obj.Actor = obj.World.Root.findByID(InstID);
            end
            OK = ~isempty(obj.Actor);
        end

        % Apply initialization script
        function OK = ApplyScript(obj, InstID) %#ok<INUSD> used in eval
            try
                World = obj.World;                                 %#ok<PROPLC> used in eval
                Actor = obj.Actor;                                 %#ok<NASGU,PROPLC> used in eval
                Observer = World.Root.Children.DefaultViewpoint;   %#ok<PROPLC,NASGU> used in eval
                Viewport = World.Viewports.Main;                   %#ok<PROPLC,NASGU> used in eval
                eval(obj.InitScript);
                OK = true;
            catch ME
                OK = false;
                error(ME.message);
            end
        end

        function OK = AssignPorts(obj, InstID)
            try
                % Input ports
                for i = 1:numel(obj.InputPorts)
                    Parts = strsplit(obj.InputPorts{i}, '.');
                    if ~isempty(Parts)
                        ChID = find(strcmp(Parts, 'Children')) + 1;
                        if isempty(ChID)
                            if strcmp(Parts{1}, '*')
                                Ref = obj.Actor.findBy('Name', Parts{2}, 'first');
                            else
                                Ref = obj.Actor;
                            end
                        else
                            Ref = eval(['obj.Actor.' strjoin(Parts(1:ChID(end)), '.')]);
                        end
                        obj.Instance(InstID).Input(i).Actor = Ref;
                        if isempty(Ref)
                            OK = false;
                        end
                    end
                end

                % Output ports
                for i = 1:numel(obj.OutputPorts)
                    Parts = strsplit(obj.OutputPorts{i}, '.');
                    if ~isempty(Parts)
                        ChID = find(strcmp(Parts, 'Children')) + 1;
                        if isempty(ChID)
                            if strcmp(Parts{1}, '*')
                                Ref = obj.Actor.findBy('Name', Parts{2}, 'first');
                            else
                                Ref = obj.Actor;
                            end
                        else
                            Ref = eval(['obj.Actor.' strjoin(Parts(1:ChID(end)), '.')]);
                        end
                        obj.Instance(InstID).Output(i).Actor = Ref;
                        if isempty(Ref)
                            OK = false;
                        end
                    end
                end
                OK = true;
            catch
                OK = false;
                % warning(message('sl3d:f3d:invalidreference', obj.InputPorts{i}));
            end
        end

        % Error handling
        function PublishError(obj, Error, InstID)
            if obj.LastError ~= Error
                switch Error
                    case 1
                        warning(message('sl3d:f3d:simsetupfailed', obj.ActorName));
                    case 2
                        warning(message('sl3d:f3d:siminstancefailed', num2str(InstID), obj.ActorName));
                end
                obj.LastError = Error;
            end
        end

    end

    methods (Static)

        % Build the world from all available blocks
        function PrepareGlobalWorld(WorldName, FullRebuild)
            GlobalWorld = F3D.World.find(WorldName);
            
            % Initialize world
            if isempty(GlobalWorld) || FullRebuild
                if F3DCompCore.shouldBeHeadless(gcs, 'Main')
                    GlobalWorld = F3D.World(WorldName, 'headless');
                else
                    GlobalWorld = F3D.World(WorldName);
                end
                GlobalWorld.SimulinkSystem = gcs;
                GlobalWorld.initialize;
                GlobalWorld.Viewports.Main.Title = WorldName;
                F3DCompCore.applyViewportSettings(gcs, GlobalWorld, 'Main');
                FullRebuild = true;
            end

            % Get proper F3D Component blocks and reset them
            Blocks = find_system(gcs, 'MaskType', 'F3D Component');
            for i = 1:numel(Blocks)
                if strcmp(get_param(Blocks{i}, 'WorldName'), WorldName)
                    Core = get_param(Blocks{i}, 'UserData');
                    Core.World = GlobalWorld;
                    Core.ShouldDoPreview = false;
                    if FullRebuild
                        Core.ResetInstances;
                    end
                else
                    Blocks{i} = [];
                end
            end

            % Compose world
            PreviousState = [];
            while ~isequal(PreviousState, cellfun(@isempty, Blocks))
                PreviousState = cellfun(@isempty, Blocks);
                for i = 1:numel(Blocks)
                    if ~isempty(Blocks{i})
                        Core = get_param(Blocks{i}, 'UserData');
                        if Core.CreateActor
                            % Create preallocated instances
                            OK = true;
                            for InstID = Core.PreallocatedInstances
                                if FullRebuild
                                    OK = Core.ManageInstance(InstID);
                                else
                                    OK = Core.ManageInstanceAtRestart(InstID);
                                end
                            end

                            % Remove other instances (from previous run)
                            ToDel = Core.LoadedInstances;
                            ToDel(Core.PreallocatedInstances) = false;
                            for InstID = find(ToDel)
                                Core.Instance(InstID).BaseActor.remove;
                                Core.Instance(InstID).Input = [];
                                Core.Instance(InstID).Output = [];
                                Core.LoadedInstances(InstID) = false;
                            end

                            if OK
                                Blocks{i} = [];
                            end
                        else
                            Blocks{i} = [];
                        end
                    end
                end
            end

            % Error handling
            for i = 1:numel(Blocks)
                if ~isempty(Blocks{i})
                    warning(message('sl3d:f3d:simblockfailed', get_param(Blocks{i}, 'Name')));
                end
            end
        end

        % Validate parameters and extract metadata
        function [Valid, PropName, GroupName, Indices] = getParamInfo(PortName)
            Valid = false;

            % Parse input to tokens
            Parts = strtrim(strsplit(PortName, '.'));
            if numel(Parts) >= 1
                % Identify user-entered indices
                PropName = Parts{end};
                PropInd = strsplit(PropName, {'(', ')'});
                if length(PropInd) == 3
                    PropName = PropInd{1};
                    Indices = eval(['[' PropInd{2} ']']);
                else
                    Indices = [];
                end

                % Try to interpret as structural property first
                ClassInfo = [];
                i = [];
                if numel(Parts) > 1
                    GroupName = Parts{end - 1};
                    switch lower(GroupName)
                        case 'joint'
                            ClassInfo = ?F3D.Type.Joint;
                        case 'pointlight'
                            ClassInfo = ?F3D.Type.PointLight;
                        case 'spotlight'
                            ClassInfo = ?F3D.Type.SpotLight;
                        case 'texturemapping'
                            ClassInfo = ?F3D.Type.TextureMapping;
                        case 'texturetransform'
                            ClassInfo = ?F3D.Type.TextureTransform;
                        case 'trail'
                            ClassInfo = ?F3D.Type.Trail;
                    end
                    if ~isempty(ClassInfo)
                        i = find(strcmpi({ClassInfo.PropertyList.Name}, PropName));
                    end
                end

                % Try to interpret as F3D.Actor property
                if isempty(i)
                    GroupName = [];
                    ClassInfo = ?F3D.Actor;
                    i = find(strcmpi({ClassInfo.PropertyList.Name}, PropName));
                end

                % Create vector of indices for supported properties (doubles only so far)
                if ~isempty(i) && strcmp(ClassInfo.PropertyList(i).Validation.Class.Name, 'double')
                    [x, y] = ClassInfo.PropertyList(i).Validation.Size.Length;
                    All = 1:double(x*y);
                    if isempty(Indices)
                        Indices = All;
                    else
                        Indices = All(Indices);
                    end
                    Valid = true;
                end
            end
        end

        function headless = shouldBeHeadless(System, ViewportName)
            headless = false;
            blocks = find_system(System, 'MaskType', 'F3D Viewer');
            for i = 1:numel(blocks)
                if strcmp(ViewportName, get_param(blocks{i}, 'Owner'))
                    headless = strcmp(get_param(blocks{i}, 'Headless'), 'on');
                    break
                end
            end
        end

        function applyViewportSettings(System, World, ViewportName)
            blocks = find_system(System, 'MaskType', 'F3D Viewer');
            for i = 1:numel(blocks)
                if strcmp(ViewportName, get_param(blocks{i}, 'Owner'))
                    title = get_param(blocks{i}, 'Title');
                    if ~isempty(title)
                        World.Viewports.(ViewportName).Title = title;
                    end
                    logo = get_param(blocks{i}, 'Logo');
                    if ~isempty(logo)
                        World.Viewports.(ViewportName).setLogo(logo);
                    end
                    break
                end
            end
        end

    end

end