function graph = functionToLayerGraph(fun,dlX,varargin)
%functionToLayerGraph  Convert function to a layerGraph
%   functionToLayerGraph will be removed in a future release. Define the network
%   using a layerGraph instead.
%
%   LGRAPH = functionToLayerGraph(FUN, dlX) evaluates FUN(dlX) and converts the
%   executed operations to a layerGraph LGRAPH.  Only operations which are
%   derived from dlarray objects among the input dlX will be converted.
%
%   LGRAPH = functionToLayerGraph(__,Name,Value) specifies additional options 
%   using one or more name-value pair arguments.
%
%   Name-Value Pair Arguments:
%     'GenerateLayer'     - Type of layer to generate for operations with no equivalent built-in layer
%                              'custom-layer' (default) | 'placeholder-layer'
%     'CustomLayerPrefix' - Prefix for generated custom layer class name
%                              'customLayer' (default) | character vector
%
%   Examples:
%      lgraph = functionToLayerGraph(@relu,dlarray(0));
%
%      function dlY = model(dlX,parameters)
%         weights = parameters.fc.Weights;
%         bias = parameters.fc.Bias;
%         dlY = fullyconnect(dlX,weights,bias);
%         dlY = softmax(dlY);
%      end
%      parameters = struct;
%      parameters.fc.Weights = dlarray(rand(10),'CB');
%      parameters.fc.Bias = dlarray(zeros(10,1),'CB');
%      fun = @(dlX) model(dlX,parameters);
%      dlX = dlarray(ones(10,1),'CB');
%      lgraph = functionToLayerGraph(fun, dlX);
%
%   See also: layerGraph, dlarray

%   Copyright 2019-2022 The MathWorks, Inc.

persistent tm;
if isempty(tm)
    tm = deep.internal.recording.TapeManager();
end

warning(message('deep:functionToLayerGraph:Deprecation'));

if ~isa(fun,'function_handle')
    error(message('deep:functionToLayerGraph:FunctionHandle'));
end

classManager = deep.internal.recording.convert.ClassFileManager;
try
    inputArguments = iParseInputArguments(varargin{:});
catch err
    throw(err);
end
generateCustomLayers = strcmp(inputArguments.GenerateLayer,'custom-layer');
classManager.Prefix = inputArguments.CustomLayerPrefix;
numOutputs = iNumberOfOutputs(fun);
dlX = deep.internal.networkContainerFun(@iPermuteToStandardLayout,dlX);
dlX = deep.internal.recording.recordContainer(dlX);

iDisableNetworkOptimization(fun);

restore = deep.internal.startTracingAndSetupCleanup(tm); %#ok
if numOutputs == 0
    fun(dlX);
    outputs = {};
else
    [outputs{1:numOutputs}] = fun(dlX);
end
tape = deep.internal.recording.tapeStruct(tm,0);
[hasPermutes, hasEmbeddingPermutes] = iContainsPermute(tape);
if hasPermutes
    if hasEmbeddingPermutes
      error(message('deep:functionToLayerGraph:EmbeddingPermutation'));
    else
      error(message('deep:functionToLayerGraph:Permutations'));
    end
end
inputDataIDs = deep.internal.recording.getIDs(dlX);
outputDataIDs = deep.internal.recording.getIDs(outputs);
if isempty(outputDataIDs) && ~isempty(tape.Data)
    outputDataIDs = tape.Data(end).Outputs;
end

% Make inputIDs into a flat list
try
    [graph,messages] = deep.internal.recording.convert.tapeToLayerGraph(tape,inputDataIDs,...
                                                      outputDataIDs,...
                                                      generateCustomLayers, classManager);
catch err
    throw(err);
end
fields = fieldnames(messages);
for i=1:length(fields)
    warning(messages.(fields{i}));
end
if any(arrayfun(@iIsPlaceholder,graph.Layers))
    warning(message('deep:functionToLayerGraph:Placeholder'));
end
end

function tf = iIsPlaceholder(layer)
tf = isa(layer,'nnet.cnn.layer.PlaceholderLayer');
end

function inputArguments = iParseInputArguments(varargin)
parser = iCreateParser();
parser.parse(varargin{:});
inputArguments = iConvertToCanonicalForm(parser.Results);
end

function p = iCreateParser()
p = inputParser;
defaultMode = 'custom-layer';
defaultPrefix = iDefaultPrefix;
p.addParameter('GenerateLayer', defaultMode, @(x)~isempty(iCheckAndReturnValidLayerMode(x)));
p.addParameter('CustomLayerPrefix', defaultPrefix, @iAssertValidPrefix);
end

function inputArguments = iConvertToCanonicalForm(params)
inputArguments = struct;
inputArguments.GenerateLayer = iCheckAndReturnValidLayerMode(params.GenerateLayer);
inputArguments.CustomLayerPrefix = iCheckAndReturnValidPrefix(params.CustomLayerPrefix);
end

function value = iCheckAndReturnValidLayerMode(value)
value = validatestring(value,{'custom-layer','placeholder-layer'});
end

function iAssertValidPrefix(value)
nnet.internal.cnn.layer.paramvalidation.isValidStringOrCharArray(value);
end

function value = iCheckAndReturnValidPrefix(value)
value = char(value);
if isempty(value)
    value = iDefaultPrefix;
end
if ~isvarname(value)
    error(message('deep:functionToLayerGraph:InvalidPrefix'));    
end
end

function str = iDefaultPrefix
str = 'customLayer';
end

function n = iNumberOfOutputs(fun)
% Determine the number of outputs to request from fun. If varargout
% is used then ask for no extra outputs execpt if varargout is the
% only output.
try
    n = nargout(fun);
catch
    n = 1;
end
if n == -1
    n = 1;
elseif n < 0
    n = abs(n) - 1;
end
end

function y = iPermuteToStandardLayout(x)
data = extractdata(x);
fmts = dims(x);
y = dlarray(data,fmts);
end

function [tf, hasEmbeddingPermute] = iContainsPermute(tape)
tf = false;
hasEmbeddingPermute = false;
entries = tape.Data;
for i = 1:length(entries)
    if iIsPermutation(entries(i).Operation)
        tf = true;
        hasEmbeddingPermute = iIsEmbeddingPermute(tape, i);
        break;
    end
end
end

function tf = iIsPermutation(op)
tf = isa(op, 'deep.internal.recording.operations.PermuteOp') || ...
    isa(op, 'deep.internal.recording.operations.IPermuteOp') || ...
    (isa(op, 'deep.internal.recording.operations.DlconvOp') && ...
    hasXPermutation(op)) || ...
    (isa(op, 'deep.internal.recording.operations.DltranspconvOp') && ...
    hasXPermutation(op)) ;
end

function tf = iIsEmbeddingPermute(tape, index)
embedOp = 'deep.internal.recording.operations.EmbedOp';
tf = index > 1 && ...
     isa(tape.Data(index - 1).Operation, embedOp);
end

function iDisableNetworkOptimization(fun)
fundata = functions(fun);
if isfield(fundata, 'workspace')
    workspaceCell = fundata.workspace;
    deep.internal.basicContainerFun(@iDisableOneNetwork,workspaceCell);
end
end

function iDisableOneNetwork(var)
if isa(var, 'dlnetwork')
    strategy = getEvaluationStrategy(var);
    disableOptimization(strategy);
elseif isa(var, 'function_handle')
    iDisableNetworkOptimization(var);
end
end
