function T = validateStackControlJSON(filenames)
% VALIDATESTACKCONTROLJSON  Validate extensions.json stackControl metadata.
%
%   VALIDATESTACKCONTROLJSON() validates a extensions.json file
%   in the current folder or a "resources" subfolder which contains metadata
%   matalb.language.stackControl
%
%   VALIDATESTACKCONTROLJSON(filenames) validates filenames. filenames
%   can be a string array, character vector, or cell array of character
%   vectors.
%
%   T = VALIDATESTACKCONTROLJSON(___) returns a table of results.

%   Copyright 2022 The MathWorks, Inc.

    narginchk(1, 1);

    filenames=convertCharsToStrings(filenames);
    log([]);
    T = log();

    for i=1:numel(filenames)
        filename = filenames(i);
        result = validateOneJsonFile(filename);
        T = [T;result]; %#ok<AGROW>
        if nargout==0 && ~isempty(result)
            disp(filename);
            disp(repmat('=', [1, strlength(filename)]));
            for j = 1:size(result,1)
                cols = result{j,'ColumnExtents'};
                if matlab.internal.display.isHot
                    location = compose("<a href=""matlab:opentoline('%s', %d, %d)"">L %d</a> (C %d-%d)", ...
                        result{j,'Filename'}, result{j,'LineNumber'}, cols(1), ...
                        result{j,'LineNumber'}, cols(1), cols(2));
                else
                    location = compose("L %d (C %d-%d)", result{j,'LineNumber'}, cols(1), cols(2));
                end
                outputMessage = location + ": " + result{j,'Message'};
                disp(outputMessage);
            end
            fprintf('\n')
        end
    end

    if nargout==0
        if isempty(T)
            disp(getString(message("MATLAB:stackControl:noOutputAtCompletion")));
        end
        clear T;
    else
        T(:,'MessageId')=[];
    end
end

function out = log(p, msgId, varargin)
    persistent validateStackControlReport

    if nargin == 0
        out = validateStackControlReport;
    elseif isempty(p) && isnumeric(p)
        validateStackControlReport = table(strings(0), [], zeros(0,2), strings(0), strings(0), ...
        'VariableNames', {'Filename', 'LineNumber', 'ColumnExtents', 'MessageId', 'Message'});
    else
        if ~contains(msgId, ':')
            msgId = "MATLAB:validateFunctionSignaturesJSON:"+msgId;
        end
        msg = message(msgId, varargin{:});
    
        if p(1).line ~= p(end).line || string(p(1).file) ~= string(p(end).file)
            p = p(end);  % Don't allow a message to span lines (or, by extension, files).
        end

        validateStackControlReport = [validateStackControlReport; ...
            {string(p(1).file), p(1).line, [p(1).cols(1) p(end).cols(2)], msgId, string(msg)}];
    end

end

function out = logParentResourcesFolder(filename)
    persistent parentResourcesFolder

    if nargin == 0
        out = parentResourcesFolder;
    else
        resourcesFolder = fileparts(char(filename));
        parentResourcesFolder = fileparts(resourcesFolder);
    end
end

function [extensionsLocation, isValid] = checkResourcesFolder(filename)
    isValid = true;
    [parentFolder, name, ext] = fileparts(char(filename));
    fullFilename = strcat(name,ext);
    [rootFolder, resources] = fileparts(parentFolder);
    extensionsLocation = fullfile(rootFolder, 'resources', fullFilename);
    p = newToken(string(filename), "", "", 1, [1 1]);

    if ext ~= ".json"
        log(p, "MATLAB:stackControl:invalidExtension");
        isValid = false;
    elseif name ~= "extensions"
        log(p, "MATLAB:stackControl:invalidFileName");
        isValid = false;
    elseif resources ~= "resources"
        log(p, "MATLAB:stackControl:jsonFileNotInResourcesFolder", filename);
        isValid = false;
    end
    
end

function text = readUTF8(filename)
    if isempty(filename)
        throw(MException(message("MATLAB:stackControl:emptyFileName")));
    elseif isempty(fileparts(filename))
        filename = fullfile('.', filename);  % Avoid opening a file on the path
    end

    fileId = fopen(char(filename),'rt','n','utf-8');

    if fileId < 0
        throw(MException(message("MATLAB:stackControl:cannotOpenFile", filename)));
    end

    text = string(fread(fileId,'*char')');
    fclose(fileId);
end

function s = newToken(filename, name, token, line, cols)
    s = struct('file', filename, 'name', name, 'token', token, 'line', line, 'cols', cols);
end

function [line, cols] = idx2lineColumn(indexes, endOfLines)
index = find(endOfLines < indexes(1), 1, 'last');
if isempty(index)
    line = 1;
    cols = indexes;
else
    line = index+1;
    cols = indexes-endOfLines(index);
end
end

function s = tokenJSON(filename, txt)
    % Assemble the regular expression
    re = strings(0);
    re(end+1) = '?<string>"([^"\\]|\\["\\/bfnrt]|\\u[0-9A-Fa-f]{4})*?"';
    re(end+1) = "?<number>\-?(0|[1-9][0-9]*)(\.[0-9]+)?([Ee][+\-]?[0-9]+)?";
    re(end+1) = "?<comma>,";
    re(end+1) = "?<colon>:";
    re(end+1) = "?<leftSquareBrace>\[";
    re(end+1) = "?<rightSquareBrace>\]";
    re(end+1) = "?<leftCurlyBrace>\{";
    re(end+1) = "?<rightCurlyBrace>\}";
    re(end+1) = "?<true>true";
    re(end+1) = "?<false>false";
    re(end+1) = "?<null>null";
    re = "(" + join(re, ")|(") + ")";
    [names, extents]=regexp(txt, re, "names", "tokenExtents");

    % Convert file offsets to line/column number
    endOfLines = find(char(txt)==newline);
    s = repmat(newToken(filename, '', '', nan, []), size(names));
    for i=1:numel(names)
        fields = fieldnames(names(i));
        for j=1:numel(fields)
            if ~isempty(names(i).(fields{j}))
                [line,cols] = idx2lineColumn(extents{i}, endOfLines);
                s(i).name = fields{j};
                s(i).token = names(i).(fields{j});
                s(i).line = line;
                s(i).cols = cols;
                if strcmp(s(i).name, 'string')
                    s(i).token = jsondecode(s(i).token);
                end
            end
        end
    end
end

function i = findEndOfValue(p, i, maxDepth, messageId, varargin)
    if nargin < 3
        maxDepth = inf;
    end
    if p(i+1).name ~= "colon"
        return
    end
    depth = 0;
    done = false;
    reportedDepthError = false;
    i = i + 2;
    while ~done
        switch p(i).name
            case {'leftCurlyBrace', 'leftSquareBrace'}
                depth = depth + 1;
                if depth > maxDepth && ~reportedDepthError
                    log(p(i), messageId, varargin{:});
                    reportedDepthError = true;
                end
            case {'rightCurlyBrace', 'rightSquareBrace'}
                depth = depth - 1;
            otherwise
        end
    
        done = depth==0;
        if ~done
            i = i + 1;
        end
    end
end

function idx = skipValue(p, idx)
    if p(idx).name == "colon"
        idx = idx + 1;
    end
end

function newIdx = validateSchema(p, idx)
    newIdx = idx - 2;
    if p(idx).token=="schemaVersion"
        [p, newIdx] = validateExpectString(p, idx+2);
        if newIdx==idx + 2
            knownSchemaVersions = "1.0.0"; % Update message catalog when this changes.
            if ~any(knownSchemaVersions == p(newIdx).token)
                log( ...
                    p(newIdx), "MATLAB:stackControl:schemaUnexpectedValue");
            end
        end
    end
end

function valid = validateExpectObject(p, idx, object)
    valid = false;
    endOfObject = findEndOfValue(p, idx);
    if p(idx).name == "string" && p(idx + 1).name == "colon" &&...
         p(idx + 2).name == "leftCurlyBrace" && ...
         p(endOfObject).name == "rightCurlyBrace" && ...
         p(idx).token == object

        valid = true;        
    else
        log(p(idx), "MATLAB:stackControl:objectExpected", object);
    end
end

function [p, idx] = validateExpectString(p, idx)
    if p(idx).name ~= "string"
        log(p(idx), "stringExpected")
        idx = skipValue(p, idx);
    end
end

function valid = validateExpectList(p, idx, listName)
    valid = false;
    endOfObject = findEndOfValue(p, idx);
    if p(idx).name == "string" && p(idx + 1).name == "colon" &&...
         p(idx + 2).name == "leftSquareBrace" && ...
         p(endOfObject).name == "rightSquareBrace" && ...
         p(idx).token == listName

        valid = true;        
    else
        log(p(idx), "MATLAB:stackControl:listExpected", listName);
    end
end

function valid = validateStringValue(p, idx, arrayName)
    valid = false;
    if p(idx).name == "string" && ~isempty(p(idx).token)
        valid = true;
    else
        log(p(idx), "MATLAB:stackControl:stringValueExpected", arrayName);
    end
end

function isValid = IsMatlabFile(ext)
    supportedExtensions = [".m", ".p", ".mlx"];
    isValid = any(supportedExtensions(:) == ext);
end

function validatePath(p, idx, filePath, arrayType)
    switch arrayType
        case "files"
            [~, ~, ext] = fileparts(filePath);
            if ~isfile(filePath) || ~exist(filePath, "file")
                log(p(idx) ,"MATLAB:stackControl:invalidPropertyPathValue", filePath, arrayType);
            end       
            if ~IsMatlabFile(ext)
                log(p(idx) ,"MATLAB:stackControl:invalidFileExtension", filePath);
            end
        case "folders"
            if ~isfolder(filePath) || ~exist(filePath, "dir")
                log(p(idx) ,"MATLAB:stackControl:invalidPropertyPathValue", filePath, arrayType);
            end
        otherwise
            log(p(idx) ,"MATLAB:stackControl:invalidPropertyPathValue", filePath, arrayType);
    end
end

function validateElementsFromArray(p, idx, endIdx, arrayType)
    idxElement = idx + 3;    
    if idxElement < endIdx
        while(idxElement < endIdx)
            stringValue = p(idxElement).token;
            isValid = validateStringValue(p, idxElement, arrayType);
            if isValid
                rootFolder = logParentResourcesFolder();
                filePath = fullfile(rootFolder, stringValue);
                validatePath(p, idxElement, filePath, arrayType);
            end
            idxElement = idxElement + 2;
        end
    end
end

function validateHiddenFramesObject(p, idx, endIdx)

    indexElement = idx + 3;
    while(indexElement < endIdx)
        [p, newIdxArray] = validateExpectString(p, indexElement);
        
        if p(newIdxArray).token == "files"
            valid = validateExpectList(p, newIdxArray, "files");
            endOfArray = findEndOfValue(p, newIdxArray);
            if valid
                validateElementsFromArray(p, newIdxArray, endOfArray, "files");
            end
            indexElement = endOfArray + 2;
        elseif p(newIdxArray).token == "folders"
            valid = validateExpectList(p, newIdxArray, "folders");
            endOfArray = findEndOfValue(p, newIdxArray);
            if valid
                validateElementsFromArray(p, newIdxArray, endOfArray, "folders");
            end
            indexElement = endOfArray + 2;
        else
            log(p(newIdxArray), "MATLAB:stackControl:attributeUnknown", p(newIdxArray).token, "hiddenFrames");
            indexElement = findEndOfValue(p, newIdxArray) + 2;
        end 
    end
end


function validateStackControl(p, idx, endIdx)
    newIdx = validateSchema(p, idx + 3) + 2;
    while(newIdx < endIdx)
        if p(newIdx).token == "hiddenFrames"
            isValidaObject = validateExpectObject(p, newIdx, "hiddenFrames");
            endOfHiddenFrames = findEndOfValue(p, newIdx);
            if(isValidaObject)
                validateHiddenFramesObject(p, newIdx, endOfHiddenFrames);
            end    
            newIdx = endOfHiddenFrames + 2;
        else
            log(p(newIdx), "MATLAB:stackControl:attributeUnknown", p(newIdx).token, "matlab.language.stackControl");
             endOfHiddenFrames = findEndOfValue(p, newIdx);
             newIdx = endOfHiddenFrames + 2;
        end
    end
end

function processAttributes(p)
    i = 1;
    isMatlabStackControlInJSON = false;
    while i < numel(p)
        if (p(i).name=="string" && ~isempty(p(i).token)) && ...
            strcmp(p(i).token, "matlab.language.stackControl")
            
            isMatlabStackControlInJSON = true;
            isValidStackControl = validateExpectObject(p, i, "matlab.language.stackControl");
            if isValidStackControl
                e = findEndOfValue(p, i);
                validateStackControl(p, i, e);
            end
            i = numel(p);
        end
        i = i + 1;
    end
    if ~isMatlabStackControlInJSON
        log(p(1), "MATLAB:stackControl:undefinedStackControl");
    end
end

function T = validateOneJsonFile(filename)
    log([]);
    [filename, isValid] = checkResourcesFolder(filename);

    if ~isValid
        T = log();
        return;
    end
    txt = string(readUTF8(char(filename)));

    try
        jsondecode(char(txt));
        json = tokenJSON(filename, char(txt));
    catch e
        matchedNumbers = double(string(regexp(e.message, '[0-9]+', 'match')));
        if numel(matchedNumbers) ~= 3
            % Report error at end of file
            endOfLines = find(char(txt)==newline);
            lineNumber = numel(endOfLines)+1;
            if isempty(endOfLines)
                endOfLines = 0;
            end
            columnNumber = strlength(txt) - endOfLines(end) + 1;
            matchedNumbers = [lineNumber, columnNumber];
        end

        s = newToken(string(filename), 'error', e.message, matchedNumbers(1), [matchedNumbers(2) matchedNumbers(2)+1]);
        log(s, "jsonParsingError", e.message);
        json = [];
    end

    if exist('json', 'var') && ~isempty(json)

        if json(1).name ~= "leftCurlyBrace"
            log(json(1), "objectExpected");
        else
            logParentResourcesFolder(filename);
            processAttributes(json);
        end
    end
    T = log();
    T = sortrows(T, {'Filename', 'LineNumber', 'ColumnExtents', 'MessageId'});
end
