function X = internal_dltranspconv(X, W, B, permX, permW, croppingSize, stride, dilation, numGroups, outputSizeOffset)
% This function is undocumented and will change in a future release.

% Applies d-dimensional transposed convolution on the GPU.
%
% Inputs:
%   X           = N1 x N2 x...x Nd x Cin x N array.
%   W           = M1 x M2 x...x Md x K x C x G array. G is the
%               number of groups and Cin=C*G.
%   B           = 1  x 1  x...x 1  x Cout array, where Cout = K*G.
%   permX       = 1 x d+2 array giving the data to view permutation of X
%                 or 1 x 0 if trivial.
%   permW       = 1 x d+3 array giving the data to view permutation of W
%                 or 1 x 0 if trivial.
%   croppingSize = [L1,H1,...,Ld,Hd] where Li and Hi are the start and end
%                 cropping sizes in the ith dimension.
%   stride      = [s1,...,sd] where si is stride in the ith dimension.
%   dilation    = [c1,...,cd] where ci is dilation in the ith dimension.
%   numGroups   = number of groups G.
%   outputSizeOffset = [o1,...,od] where oi is the offset in the ith
%                      dimension.
%
% Outputs:
%   Z           = O1 x O2 x...x Od x Cout x N array.

%   Copyright 2019-2022 The MathWorks, Inc.

if ~isreal(X) || ~isreal(W) || ~isreal(B)
    error(message('deep:dlarray:ComplexNotSupported'))
end

% Ensure all inputs have the same data type
[X, W, B] = nnet.internal.cnngpu.castToSuperiorFloat(X, W, B);

[croppingStart, croppingEnd, isCropSymmetric] = deep.internal.dlarray.numeric.convertBorderToHostFormat(croppingSize);
% If cropping is not symmetric, replace with zeros to prevent the builtin
% from cropping
numConvolutionDims = numel(stride);
if ~isCropSymmetric
    croppingStart = zeros(1,numConvolutionDims);
    croppingEnd = zeros(1,numConvolutionDims);
end

% Compute the output size
outputSize = deep.internal.dlarray.numeric.computeDltranspconvOutputSize(X, W, permX, permW, numConvolutionDims, numGroups, croppingStart, croppingEnd, stride, dilation, outputSizeOffset);

% Permute weights to the canonical order
if ~isempty(permW)
    W = permute(W,permW);
end
if numGroups > 1
    W = deep.internal.dlarray.numeric.groupConvolutionFilters(W,numConvolutionDims,numGroups);
end
% Permute input data and loss
isPermXNonEmpty = ~isempty(permX);
if isPermXNonEmpty
    X = permute(X,permX);
end

% Call the backward convolution builtin method with X in place of dZ 
X = nnet.internal.cnngpu.convolveBackwardDataNDCore(...
    outputSize, W, X, croppingStart, [], stride, dilation, numGroups);

% If cropping is not symmetric, we need to unpad outside of the CuDNN method
if ~isCropSymmetric
    X = iUnpadArray(X, croppingSize);
end

% Add bias
X = X + B;
if isPermXNonEmpty
    X = ipermute(X,permX);
end
end

function outputArray = iUnpadArray(inputArray, croppingSize)
% This method does not support more than 5D arrays
outputArray = nnet.internal.cnnhost.padding.unpadArray(inputArray, croppingSize);
end
