% Main layout-manager class
classdef GridLayout < handle

    properties
        Container
        
        NumRows
        NumCols
        RowHeight
        ColWidth
        HGap
        VGap
        LMargin
        RMargin
        TMargin
        BMargin
        
        % Sizing policies
        % 0: Absolute
        % 1: Proportional
        % 2: Automatic

        RowHeightPolicy
        RowHeightAbsolute
        RowHeightWeight
        
        ColWidthPolicy
        ColWidthAbsolute
        ColWidthWeight
    end
    
    properties (Constant)
        MinRowHeight = 5
        MinColWidth = 5
    end
    
    properties (Dependent)
        ActualWidth
        ActualHeight
        RequiredWidth
        RequiredHeight
    end
    
    properties (SetAccess = private)
        Cell  % Cell container
    end
    
    methods
        function Obj = GridLayout(Parent, varargin)
            Names = { ...
                'NumRows' ...
                'NumCols' ...
                'RowHeight' ...
                'ColWidth' ...
                'CellColor' ...
                'Gap' ...
                'HGap' ...
                'VGap' ...
                'Margin' ...
                'LMargin' ...
                'RMargin' ...
                'TMargin' ...
                'BMargin'};
            NamesCell = CellProps.GetCellParamNames();
            [P, PC] = ParseArgs(varargin, Names, NamesCell);
            
            assert(~isempty(Parent), ...
                'Parent handle cannot be empty.');

            Obj.Container = uicontainer( ...
                'Parent', Parent, ...
                'ResizeFcn', @(hsrc,ev)Update(Obj,true), ...
                'BackgroundColor', P.CellColor);
            Position = getpixelposition(Obj.Container);
            setappdata(Obj.Container,'Position',Position);

            % NumRows
            DefaultNumRows = 1;
            if ~isempty(P.RowHeight) && iscell(P.RowHeight)
                DefaultNumRows = length(P.RowHeight);
            end
            Obj.NumRows = GetArg(P,'NumRows',DefaultNumRows);
            % NumCols
            DefaultNumCols = 1;
            if ~isempty(P.ColWidth) && iscell(P.ColWidth)
                DefaultNumCols = length(P.ColWidth);
            end
            Obj.NumCols = GetArg(P,'NumCols',DefaultNumCols);
            % RowHeight
            Value = GetArg(P,'RowHeight','*');
            if ~iscell(Value)
                Obj.RowHeight = cell(1,Obj.NumRows);
                [Obj.RowHeight{1:end}] = deal(Value);
            else
                Obj.RowHeight = P.RowHeight;
            end
            % ColWidth
            Value = GetArg(P,'ColWidth','*');
            if ~iscell(Value)
                Obj.ColWidth = cell(1,Obj.NumCols);
                [Obj.ColWidth{1:end}] = deal(Value);
            else
                Obj.ColWidth = P.ColWidth;
            end
            % HGap
            Obj.HGap = GetArg(P,'HGap',5);
            % VGap
            Obj.VGap = GetArg(P,'VGap',5);
            % Gap (overrides HGap, VGap)
            if ~isempty(P.Gap)
                Obj.HGap = P.Gap;
                Obj.VGap = P.Gap;
            end
            % Margin
            if isempty(P.Margin)
                DefaultLMargin = Obj.HGap;
                DefaultRMargin = Obj.HGap;
                DefaultTMargin = Obj.VGap;
                DefaultBMargin = Obj.VGap;
            else
                DefaultLMargin = P.Margin;
                DefaultRMargin = P.Margin;
                DefaultTMargin = P.Margin;
                DefaultBMargin = P.Margin;
            end
            Obj.LMargin = GetArg(P,'LMargin',DefaultLMargin);
            Obj.RMargin = GetArg(P,'RMargin',DefaultRMargin);
            Obj.TMargin = GetArg(P,'TMargin',DefaultTMargin);
            Obj.BMargin = GetArg(P,'BMargin',DefaultBMargin);
            
            % Create array of cell containers
            Obj.Cell = zeros(Obj.NumRows,Obj.NumCols);
            for RIdx = 1:Obj.NumRows
                for CIdx = 1:Obj.NumCols
                    ThisCell = uicontainer( ...
                        'Parent', Obj.Container, ...
                        'Units', 'pixels');
                    if ~isempty(P.CellColor)
                        set(ThisCell, 'BackgroundColor', P.CellColor);
                    end
                    Props = CellProps(PC);
                    setappdata(ThisCell,'Props',Props);
                    setappdata(ThisCell,'RSpan',[RIdx RIdx]);
                    setappdata(ThisCell,'CSpan',[CIdx CIdx]);
                    Obj.Cell(RIdx,CIdx) = ThisCell;
                end
            end

            % RowHeightPolicy
            Obj.RowHeightPolicy   = zeros(1,Obj.NumRows);
            Obj.RowHeightAbsolute = zeros(1,Obj.NumRows);
            Obj.RowHeightWeight   = zeros(1,Obj.NumRows);
            for RIdx = 1:Obj.NumRows
                RHeight = Obj.RowHeight{RIdx};
                if isnumeric(RHeight)
                    % Absolute
                    Obj.RowHeightPolicy(RIdx) = 0;
                    Obj.RowHeightAbsolute(RIdx) = max(RHeight,Obj.MinRowHeight);
                elseif ischar(RHeight) && RHeight(end) == '*'
                    % Proportional
                    Obj.RowHeightPolicy(RIdx) = 1;
                    Weight = RHeight(1:end-1);
                    if isempty(Weight)
                        Weight = '1';
                    end
                    Obj.RowHeightWeight(RIdx) = str2double(Weight);
                elseif ischar(RHeight) && isequal(RHeight,'a')
                    % Automatic
                    Obj.RowHeightPolicy(RIdx) = 2;
                else
                    error('Illegal RowHeight definition.');
                end
            end
            
            % ColWidthPolicy
            Obj.ColWidthPolicy   = zeros(1,Obj.NumCols);
            Obj.ColWidthAbsolute = zeros(1,Obj.NumCols);
            Obj.ColWidthWeight   = zeros(1,Obj.NumCols);
            for CIdx = 1:Obj.NumCols
                CWidth = Obj.ColWidth{CIdx};
                if isnumeric(CWidth)
                    % Absolute
                    Obj.ColWidthPolicy(CIdx) = 0;
                    Obj.ColWidthAbsolute(CIdx) = max(CWidth,Obj.MinColWidth);
                elseif ischar(CWidth) && CWidth(end) == '*'
                    % Proportional
                    Obj.ColWidthPolicy(CIdx) = 1;
                    Weight = CWidth(1:end-1);
                    if isempty(Weight)
                        Weight = '1';
                    end
                    Obj.ColWidthWeight(CIdx) = str2double(Weight);
                elseif ischar(CWidth) && isequal(CWidth,'a')
                    % Automatic
                    Obj.ColWidthPolicy(CIdx) = 2;
                else
                    error('Illegal ColWidth definition.');
                end
            end
            
            if ~isempty(Obj.RequiredHeight)
                HeightDiff = Obj.RequiredHeight-Obj.ActualHeight;
                Position = getpixelposition(Parent);
                Position(4) = Position(4) + HeightDiff;
                setpixelposition(Parent, Position);
            end
        end

        function MergeCells(Obj, RSpan, CSpan)
            if isempty(RSpan)
                RSpan = [1 Obj.NumRows];
            end
            if isscalar(RSpan)
                RSpan = [RSpan RSpan];
            end
            if isempty(CSpan)
                CSpan = [1 Obj.NumCols];
            end
            if isscalar(CSpan)
                CSpan = [CSpan CSpan];
            end
            for RIdx = RSpan(1):RSpan(2)
                for CIdx = CSpan(1):CSpan(2)
                    ThisCell = Obj.Cell(RIdx,CIdx);
                    if RIdx == RSpan(1) && CIdx == CSpan(1)
                        setappdata(ThisCell,'RSpan',RSpan);
                        setappdata(ThisCell,'CSpan',CSpan);
                    else
                        delete(ThisCell);
                    end
                end
            end
        end

        function RemoveCells(Obj, RSpan, CSpan)
            if isempty(RSpan)
                RSpan = [1 Obj.NumRows];
            end
            if isscalar(RSpan)
                RSpan = [RSpan RSpan];
            end
            if isempty(CSpan)
                CSpan = [1 Obj.NumCols];
            end
            if isscalar(CSpan)
                CSpan = [CSpan CSpan];
            end
            for RIdx = RSpan(1):RSpan(2)
                for CIdx = CSpan(1):CSpan(2)
                    ThisCell = Obj.Cell(RIdx,CIdx);
                    if ishghandle(ThisCell)
                        delete(ThisCell);
                    end
                end
            end
        end

        function FormatCells(Obj, RSpan, CSpan, varargin)
            if isscalar(RSpan)
                RSpan = [RSpan RSpan];
            end
            if isscalar(CSpan)
                CSpan = [CSpan CSpan];
            end
            for RIdx = RSpan(1):RSpan(2)
                for CIdx = CSpan(1):CSpan(2)
                    ThisCell = Obj.Cell(RIdx,CIdx);
                    if ~ishghandle(ThisCell)
                        continue; % Cell is removed
                    end
                    GridLayout.FormatCell(ThisCell, varargin{:});
                end
            end
        end

        function Update(Obj, IsCallback)
            if nargin < 2
                IsCallback = false;
            end
            % Layout size
            NewPosition = getpixelposition(Obj.Container);
            OldPosition = getappdata(Obj.Container,'Position');
            if isequal(NewPosition,OldPosition) && IsCallback
                return;
            end
            setappdata(Obj.Container,'Position',NewPosition);
            LayoutWidth  = NewPosition(3);
            LayoutHeight = NewPosition(4);

            % Get children cells; as a vector!
            Cells = get(Obj.Container,'Children');
            % Get their row and column spans
            RSpan = zeros(length(Cells),2);
            CSpan = zeros(length(Cells),2);
            for i = 1:length(Cells)
                RSpan(i,:) = getappdata(Cells(i),'RSpan');
                CSpan(i,:) = getappdata(Cells(i),'CSpan');
            end
            
            % Determine column widths
            CWidthSum = LayoutWidth-(Obj.NumCols-1)*Obj.VGap-Obj.LMargin-Obj.RMargin;
            CWidth = zeros(1,Obj.NumCols);
            % 1. Absolute
            CMask = Obj.ColWidthPolicy == 0;
            CWidth(CMask) = Obj.ColWidthAbsolute(CMask);
            % 2. Automatic
            for CIdx = find(Obj.ColWidthPolicy == 2)
                MaxWidth = 0;
                Mask = CSpan(:,1) == CIdx & CSpan(:,2) == CIdx;
                assert(sum(Mask) > 0, ...
                    'Any column with automatic width must have at least one non-merged cell.');
                for MyCell = Cells(Mask)'
                    Props = getappdata(MyCell,'Props');
                    Width = Props.LMargin+Props.RMargin;
                    Child = get(MyCell,'Children');
                    if ~isempty(Child)
                        ChildSize = get(Child,'Position');
                        Width = Width + ChildSize(3);
                    end
                    MaxWidth = max(MaxWidth,Width);
                end
                CWidth(CIdx) = MaxWidth;
            end
            % 3. Proportional
            CWidthProportionalMask = Obj.ColWidthPolicy == 1;
            CWidthSumProportional = CWidthSum - sum(CWidth);
            CWidthSumProportional = max(CWidthSumProportional,0);
            CWidthWeight = Obj.ColWidthWeight(CWidthProportionalMask);
            CWidth(CWidthProportionalMask) = CWidthSumProportional*CWidthWeight/sum(CWidthWeight);
            CWidth = max(CWidth, Obj.MinColWidth);

            % Determine row heights
            RHeightSum = LayoutHeight-(Obj.NumRows-1)*Obj.HGap-Obj.TMargin-Obj.BMargin;
            RHeight = zeros(1,Obj.NumRows);
            % 1. Absolute
            RMask = Obj.RowHeightPolicy == 0;
            RHeight(RMask) = Obj.RowHeightAbsolute(RMask);
            % 2. Automatic
            for RIdx = find(Obj.RowHeightPolicy == 2)
                MaxHeight = 0;
                Mask = RSpan(:,1) == RIdx & RSpan(:,2) == RIdx;
                assert(sum(Mask) > 0, ...
                    'Any row with automatic height must have at least one non-merged cell.');
                for MyCell = Cells(Mask)'
                    Props = getappdata(MyCell,'Props');
                    Height = Props.TMargin+Props.BMargin;
                    Child = get(MyCell,'Children');
                    if ~isempty(Child)
                        ChildSize = get(Child,'Position');
                        Height = Height + ChildSize(4);
                    end
                    MaxHeight = max(MaxHeight,Height);
                end
                RHeight(RIdx) = MaxHeight;
            end
            % 3. Proportional
            RMask = Obj.RowHeightPolicy == 1;
            RHeightSumProportional = RHeightSum - sum(RHeight);
            RHeightSumProportional = max(RHeightSumProportional,0);
            Weight = Obj.RowHeightWeight(RMask);
            RHeight(RMask) = RHeightSumProportional*Weight/sum(Weight);
            RHeight = max(RHeight, Obj.MinRowHeight);
            
            % Horizontal and vertical offsets for each cell
            CellOffsetH = cumsum([Obj.LMargin CWidth(1:end-1)+Obj.VGap]);
            CellOffsetV = LayoutHeight-cumsum([RHeight(1)+Obj.BMargin RHeight(2:end)+Obj.HGap]);

            % Set cell positions
            for i = 1:length(Cells)
                RIdx  = RSpan(i,1):RSpan(i,2);
                CIdx  = CSpan(i,1):CSpan(i,2);

                set(Cells(i), ...
                    'Position', [ ...
                        CellOffsetH(CIdx(1)) ...
                        CellOffsetV(RIdx(end)) ...
                        sum(CWidth(CIdx))+Obj.VGap*(length(CIdx)-1) ...
                        sum(RHeight(RIdx))+Obj.HGap*(length(RIdx)-1)]);
                    
                GridLayout.ResizeCell(Cells(i));
            end
        end
        
        function Value = get.ActualWidth(Obj)
            Position = getpixelposition(Obj.Container);
            Value = Position(3);
        end
        function Value = get.ActualHeight(Obj)
            Position = getpixelposition(Obj.Container);
            Value = Position(4);
        end
        
        function Value = get.RequiredWidth(Obj)
            if any(Obj.ColWidthPolicy == 1)
                Value = [];
                return;
            end
            Mask = Obj.ColWidthPolicy == 0;
            Value = sum(Obj.ColWidthAbsolute(Mask)) + Obj.HGap*(Obj.NumCols-1) + Obj.LMargin + Obj.RMargin;
        end
        function Value = get.RequiredHeight(Obj)
            if any(Obj.RowHeightPolicy == 1)
                Value = [];
                return;
            end
            Mask = Obj.RowHeightPolicy == 0;
            Value = sum(Obj.RowHeightAbsolute(Mask)) + Obj.VGap*(Obj.NumRows-1) + Obj.TMargin + Obj.BMargin;
        end
    end
    
    methods (Static)
        function FormatCell(Cell, varargin)
            assert(isscalar(Cell) && ishghandle(Cell), ...
                'Cell must be a hghandle.');
            Props = getappdata(Cell,'Props');
            for i = 1:2:length(varargin)
                Props.(varargin{i}) = varargin{i+1};
            end
            setappdata(Cell,'Props',Props);
        end
    end
    
    methods (Static, Access = private)
        function ResizeCell(Src)
            Child = get(Src,'Children');
            if isempty(Child)
                return;
            end
            
            % Special case for axes with legend
            % The legend has the same parent as the axes!
            if ~isscalar(Child)
                assert(numel(Child) == 2 && ...
                    ishghandle(Child(1),'axes') && strcmp(get(Child(1),'Tag'),'legend') && ...
                    ishghandle(Child(2),'axes'), ...
                    'If Cell has two children, they must be an ''axes'' and its legend.');
                Child = Child(2);
            end
            
            % Special treatment for axes objects
            IsChildAxes = ishghandle(Child,'axes');
            if IsChildAxes
                OldAxesUnits = get(Child,'Units');
                set(Child,'Units','Pixels');
                UseOuterPosition = strcmp(get(Child,'ActivePositionProperty'),'outerposition');
                if UseOuterPosition
                    ChildPosition = get(Child,'OuterPosition');
                else
                    ChildPosition = get(Child,'Position');
                end
            else
                ChildPosition = get(Child,'Position');
            end
            
            % Child offset and size
            ChildOffset = ChildPosition(1:2);
            ChildSize = ChildPosition(3:4);
            % Cell size
            CellPosition = getpixelposition(Src);
            CellSize = CellPosition(3:4);
            % Cell propoerties
            Props = getappdata(Src,'Props');
            % Horizontal alignment
            HAlign  = Props.HAlign;
            LMargin = Props.LMargin;
            RMargin = Props.RMargin;
            if strcmp(HAlign,'Left')
                ChildOffset(1) = LMargin;
            elseif strcmp(HAlign,'Right')
                ChildOffset(1) = CellSize(1)-ChildSize(1)-RMargin;
            elseif strcmp(HAlign,'Center')
                ChildOffset(1) = (CellSize(1)-ChildSize(1))/2;
            else % Stretch
                ChildOffset(1) = LMargin;
                ChildSize(1) = CellSize(1)-LMargin-RMargin;
            end
            % Vertical alignment
            VAlign  = Props.VAlign;
            TMargin = Props.TMargin;
            BMargin = Props.BMargin;
            if strcmp(VAlign,'Bottom')
                ChildOffset(2) = BMargin;
            elseif strcmp(VAlign,'Top')
                ChildOffset(2) = CellSize(2)-ChildSize(2)-TMargin;
            elseif strcmp(VAlign,'Center')
                ChildOffset(2) = (CellSize(2)-ChildSize(2))/2;
            else % Stretch
                ChildOffset(2) = BMargin;
                ChildSize(2) = CellSize(2)-TMargin-BMargin;
            end
            % Prevent non-positive sizes
            ChildSize = max(ChildSize,.1);

            % Update child position
            ChildPosition = [ChildOffset ChildSize];
            if IsChildAxes
                if UseOuterPosition
                    set(Child,'OuterPosition',ChildPosition);
                else
                    set(Child,'Position',ChildPosition);
                end
                set(Child,'Units',OldAxesUnits);
            else
                set(Child,'Position',ChildPosition);
            end
        end
        
    end
end

% EOF
