/*
 * Copyright (c) 2019, Alliance for Open Media. All rights reserved
 *
 * This source code is subject to the terms of the BSD 2 Clause License and
 * the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License
 * was not distributed with this source code in the LICENSE file, you can
 * obtain it at www.aomedia.org/license/software. If the Alliance for Open
 * Media Patent License 1.0 was not distributed with this source code in the
 * PATENTS file, you can obtain it at www.aomedia.org/license/patent.
 */

#include <assert.h>
#include <math.h>

#include "aom_dsp/aom_dsp_common.h"
#include "av1/common/av1_common_int.h"
#include "av1/encoder/cnn.h"

#define CLAMPINDEX(a, hi) ((a) < 0 ? 0 : ((a) >= (hi) ? ((hi)-1) : (a)))

typedef struct {
  const float **input;
  int in_width;
  int in_height;
  int in_stride;
  const CNN_LAYER_CONFIG *layer_config;
  float **output;
  int out_stride;
  int start_idx;
  int th_step;
} CONVOLVE_OPS;

typedef float (*activation_fn)(float);

static float softsign(float x) { return x / (float)(fabsf(x) + 1.0); }

static float relu(float x) { return (x < 0) ? 0 : x; }

static float identity(float x) { return x; }

typedef struct {
  int allocsize;
  int channels;
  int width, height, stride;
  float *buf[CNN_MAX_CHANNELS];
} TENSOR;

static void init_tensor(TENSOR *tensor) { memset(tensor, 0, sizeof(*tensor)); }

static void free_tensor(TENSOR *tensor) {
  if (tensor->allocsize) {
    aom_free(tensor->buf[0]);
    tensor->buf[0] = NULL;
    tensor->allocsize = 0;
  }
}

static void realloc_tensor(TENSOR *tensor, int channels, int width,
                           int height) {
  const int newallocsize = channels * width * height;
  if (tensor->allocsize < newallocsize) {
    free_tensor(tensor);
    tensor->buf[0] =
        (float *)aom_malloc(sizeof(*tensor->buf[0]) * newallocsize);
    tensor->allocsize = newallocsize;
  }
  tensor->width = width;
  tensor->height = height;
  tensor->stride = width;
  tensor->channels = channels;
  for (int c = 1; c < channels; ++c)
    tensor->buf[c] = &tensor->buf[0][c * width * height];
}

static void copy_tensor(const TENSOR *src, int copy_channels, int dst_offset,
                        TENSOR *dst) {
  assert(src->width == dst->width);
  assert(src->height == dst->height);
  assert(copy_channels <= src->channels);
  if (src->stride == dst->width && dst->stride == dst->width) {
    for (int c = 0; c < copy_channels; ++c) {
      memcpy(dst->buf[dst_offset + c], src->buf[c],
             sizeof(*dst->buf[0]) * src->width * src->height);
    }
  } else {
    for (int c = 0; c < copy_channels; ++c) {
      for (int r = 0; r < dst->height; ++r) {
        memcpy(&dst->buf[dst_offset + c][r * dst->stride],
               &src->buf[c][r * src->stride],
               dst->width * sizeof(*dst->buf[c]));
      }
    }
  }
}

static void assign_tensor(TENSOR *tensor, float *buf[CNN_MAX_CHANNELS],
                          int channels, int width, int height, int stride) {
  tensor->allocsize = 0;
  tensor->channels = channels;
  tensor->width = width;
  tensor->height = height;
  tensor->stride = stride;
  if (buf) {
    for (int c = 0; c < channels; ++c) tensor->buf[c] = buf[c];
  } else {
    for (int c = 0; c < channels; ++c) tensor->buf[c] = NULL;
  }
}

static void swap_tensor(TENSOR *t1, TENSOR *t2) {
  TENSOR t = *t1;
  *t1 = *t2;
  *t2 = t;
}

// The concatenated tensor goes into dst with first the channels in
// original dst followed by the channels in the src
static void concat_tensor(const TENSOR *src, TENSOR *dst) {
  assert(src->width == dst->width);
  assert(src->height == dst->height);

  const int dst_channels = dst->channels;
  const int channels = dst->channels + src->channels;
  const int newallocsize = channels * dst->width * dst->height;
  if (dst->allocsize < newallocsize) {
    TENSOR t;
    init_tensor(&t);
    // allocate new buffers and copy first the dst channels
    realloc_tensor(&t, channels, dst->width, dst->height);
    copy_tensor(dst, dst->channels, 0, &t);
    // Swap the tensors and free the old buffers
    swap_tensor(dst, &t);
    free_tensor(&t);
  }
  for (int c = 1; c < channels; ++c)
    dst->buf[c] = &dst->buf[0][c * dst->width * dst->height];
  // Copy the channels in src after the first dst_channels channels.
  copy_tensor(src, src->channels, dst_channels, dst);
}

int check_tensor_equal_dims(TENSOR *t1, TENSOR *t2) {
  return (t1->width == t2->width && t1->height == t2->height);
}

int check_tensor_equal_size(TENSOR *t1, TENSOR *t2) {
  return (t1->channels == t2->channels && t1->width == t2->width &&
          t1->height == t2->height);
}

void av1_find_cnn_layer_output_size(int in_width, int in_height,
                                    const CNN_LAYER_CONFIG *layer_config,
                                    int *out_width, int *out_height) {
  if (!layer_config->deconvolve) {
    switch (layer_config->pad) {
      case PADDING_SAME_ZERO:
      case PADDING_SAME_REPLICATE:
        *out_width = (in_width + layer_config->skip_width - 1) /
                     layer_config->skip_width;
        *out_height = (in_height + layer_config->skip_height - 1) /
                      layer_config->skip_height;
        break;
      case PADDING_VALID:
        *out_width =
            (in_width - layer_config->filter_width + layer_config->skip_width) /
            layer_config->skip_width;
        *out_height = (in_height - layer_config->filter_height +
                       layer_config->skip_height) /
                      layer_config->skip_height;
        break;
      default: assert(0 && "Unknown padding type");
    }
  } else {
    switch (layer_config->pad) {
      case PADDING_SAME_ZERO:
      case PADDING_SAME_REPLICATE:
        *out_width = in_width * layer_config->skip_width;
        *out_height = in_height * layer_config->skip_height;
        break;
      case PADDING_VALID:
        *out_width = (in_width - 1) * layer_config->skip_width +
                     layer_config->filter_width;
        *out_height = (in_height - 1) * layer_config->skip_height +
                      layer_config->filter_height;
        break;
      default: assert(0 && "Unknown padding type");
    }
  }
}

void find_cnn_out_channels(const CNN_LAYER_CONFIG *layer_config,
                           int channels_per_branch[]) {
  int branch = layer_config->branch;
  const CNN_BRANCH_CONFIG *branch_config = &layer_config->branch_config;
  for (int b = 0; b < CNN_MAX_BRANCHES; ++b) {
    if ((branch_config->input_to_branches & (1 << b)) && b != branch) {
      if (layer_config->branch_copy_type == BRANCH_INPUT) {
        channels_per_branch[b] = layer_config->in_channels;
      } else if (layer_config->branch_copy_type == BRANCH_OUTPUT) {
        channels_per_branch[b] = layer_config->out_channels;
      } else if (layer_config->branch_copy_type == BRANCH_COMBINED) {
        channels_per_branch[b] = layer_config->out_channels;
        for (int c = 0; c < CNN_MAX_BRANCHES; ++c) {
          if ((branch_config->branches_to_combine & (1 << c)) && c != branch) {
            assert(channels_per_branch[c] > 0);
            channels_per_branch[b] += channels_per_branch[c];
          }
        }
      }
    }
  }
  channels_per_branch[branch] = layer_config->out_channels;
  for (int c = 0; c < CNN_MAX_BRANCHES; ++c) {
    if ((branch_config->branches_to_combine & (1 << c)) && c != branch) {
      assert(channels_per_branch[c] > 0);
      channels_per_branch[branch] += channels_per_branch[c];
    }
  }
}

#if CONFIG_DEBUG
static INLINE int cnn_has_at_least_one_output(const CNN_CONFIG *cnn_config) {
  const int num_layers = cnn_config->num_layers;
  const CNN_LAYER_CONFIG *layer_configs = cnn_config->layer_config;

  for (int idx = 0; idx < num_layers; idx++) {
    if (layer_configs[idx].output_num != -1) {
      return 1;
    }
  }
  return 0;
}
#endif

void av1_find_cnn_output_size(int in_width, int in_height,
                              const CNN_CONFIG *cnn_config, int *out_width,
                              int *out_height, int *out_channels) {
  int channels_per_branch[CNN_MAX_BRANCHES] = { 0 };
  int i_width[CNN_MAX_BRANCHES] = { 0 };
  int i_height[CNN_MAX_BRANCHES] = { 0 };
  i_width[0] = in_width + cnn_config->ext_width * 2;
  i_height[0] = in_height + cnn_config->ext_height * 2;

#if CONFIG_DEBUG
  assert(cnn_has_at_least_one_output(cnn_config));
#endif

  for (int i = 0; i < cnn_config->num_layers; ++i) {
    const CNN_LAYER_CONFIG *layer_config = &cnn_config->layer_config[i];
    const CNN_BRANCH_CONFIG *branch_config = &layer_config->branch_config;
    const int branch = layer_config->branch;
    int o_width = 0, o_height = 0;

    if (layer_config->branch_copy_type == BRANCH_INPUT) {
      for (int b = 0; b < CNN_MAX_BRANCHES; ++b) {
        if ((branch_config->input_to_branches & (1 << b)) && b != branch) {
          assert(i_width[branch] > 0 && i_height[branch] > 0);
          i_width[b] = i_width[branch];
          i_height[b] = i_height[branch];
        }
      }
    }

    av1_find_cnn_layer_output_size(i_width[branch], i_height[branch],
                                   layer_config, &o_width, &o_height);
    i_width[branch] = o_width;
    i_height[branch] = o_height;

    if (layer_config->branch_copy_type == BRANCH_OUTPUT) {
      for (int b = 0; b < CNN_MAX_BRANCHES; ++b) {
        if ((branch_config->input_to_branches & (1 << b)) && b != branch) {
          i_width[b] = o_width;
          i_height[b] = o_height;
        }
      }
    }

    find_cnn_out_channels(layer_config, channels_per_branch);

    const int output_num = layer_config->output_num;
    if (output_num != -1) {  // Current layer is an output layer
      out_width[output_num] = o_width;
      out_height[output_num] = o_height;
      out_channels[output_num] = channels_per_branch[layer_config->branch];
    }
  }
}

activation_fn get_activation(ACTIVATION layer_activation) {
  switch (layer_activation) {
    case NONE: return identity;
    case RELU: return relu;
    case SOFTSIGN: return softsign;
    case SIGMOID:
      assert(0 && "Sigmoid has not been supported in CNN.");  // TO DO
      return NULL;
    default: assert(0 && "Unknown activation type"); return NULL;
  }
}

static INLINE int get_start_shift_convolve(int width, int filt_width,
                                           int stride) {
  const int mod = (width % stride);
  const int filt_off = (filt_width - 1) / 2;
  const int dif = (mod ? mod - 1 : stride - 1);
  return AOMMIN((dif + (filt_width % 2)) / 2, filt_off);
}

void av1_cnn_add_c(float **output, int channels, int width, int height,
                   int stride, const float **add) {
  for (int c = 0; c < channels; ++c) {
    for (int i = 0; i < height; ++i)
      for (int j = 0; j < width; ++j)
        output[c][i * stride + j] += add[c][i * stride + j];
  }
}

void av1_cnn_activate_c(float **output, int channels, int width, int height,
                        int stride, ACTIVATION layer_activation) {
  activation_fn activation = get_activation(layer_activation);
  for (int c = 0; c < channels; ++c) {
    for (int i = 0; i < height; ++i)
      for (int j = 0; j < width; ++j)
        output[c][i * stride + j] = activation(output[c][i * stride + j]);
  }
}

static void copy_active_tensor_to_branches(const TENSOR *layer_active_tensor,
                                           const CNN_LAYER_CONFIG *layer_config,
                                           int branch, TENSOR branch_output[]) {
  const CNN_BRANCH_CONFIG *branch_config = &layer_config->branch_config;
  for (int b = 0; b < CNN_MAX_BRANCHES; ++b) {
    if ((branch_config->input_to_branches & (1 << b)) && b != branch) {
      // Copy layer's active tensor to output tensor of branch b if set in
      // mask. The output becomes the input of the first layer of the branch
      // because the layer of the branch is not the first layer.
      int copy_channels = branch_config->channels_to_copy > 0
                              ? branch_config->channels_to_copy
                              : layer_active_tensor->channels;
      realloc_tensor(&branch_output[b], copy_channels,
                     layer_active_tensor->width, layer_active_tensor->height);
      copy_tensor(layer_active_tensor, copy_channels, 0, &branch_output[b]);
    }
  }
}

// CNNConvolve specific to maxpool set as 1, either skip_width or skip_height
// greater than 1 and padding equal to PADDING_SAME_ZERO.
static void convolve_maxpool_padding_zero(
    const float **input, int in_width, int in_height, int in_stride,
    const CNN_LAYER_CONFIG *const layer_config, float **output, int out_stride,
    const int cstep, const int filter_width_half,
    const int filter_height_half) {
  for (int i = 0; i < layer_config->out_channels; ++i) {
    for (int h = 0, u = 0; h < in_height; h += layer_config->skip_height, ++u) {
      for (int w = 0, v = 0; w < in_width; w += layer_config->skip_width, ++v) {
        for (int hh = h; hh < AOMMIN(in_height, h + layer_config->skip_height);
             ++hh) {
          for (int ww = w; ww < AOMMIN(in_width, w + layer_config->skip_width);
               ++ww) {
            float sum = layer_config->bias[i];
            for (int k = 0; k < layer_config->in_channels; ++k) {
              int off = k * layer_config->out_channels + i;
              for (int l = 0; l < layer_config->filter_height; ++l) {
                const int ii = hh + l - filter_height_half;
                for (int m = 0; m < layer_config->filter_width;
                     ++m, off += cstep) {
                  const int jj = ww + m - filter_width_half;
                  if (ii < 0 || ii >= in_height || jj < 0 || jj >= in_width)
                    continue;
                  sum += layer_config->weights[off] *
                         input[k][ii * in_stride + jj];
                }
              }
            }
            const float a = sum;
            if (h == hh && w == ww)
              output[i][u * out_stride + v] = a;
            else
              output[i][u * out_stride + v] =
                  AOMMAX(output[i][u * out_stride + v], a);
          }
        }
      }
    }
  }
}

// CNNConvolve specific to maxpool set as 1, either skip_width or skip_height
// greater than 1 and padding equal to PADDING_SAME_REPLICATE.
static void convolve_maxpool_padding_replicate(
    const float **input, int in_width, int in_height, int in_stride,
    const CNN_LAYER_CONFIG *const layer_config, float **output, int out_stride,
    const int cstep, const int filter_width_half,
    const int filter_height_half) {
  for (int i = 0; i < layer_config->out_channels; ++i) {
    for (int h = 0, u = 0; h < in_height; h += layer_config->skip_height, ++u) {
      for (int w = 0, v = 0; w < in_width; w += layer_config->skip_width, ++v) {
        for (int hh = h; hh < AOMMIN(in_height, h + layer_config->skip_height);
             ++hh) {
          for (int ww = w; ww < AOMMIN(in_width, w + layer_config->skip_width);
               ++ww) {
            float sum = layer_config->bias[i];
            for (int k = 0; k < layer_config->in_channels; ++k) {
              int off = k * layer_config->out_channels + i;
              for (int l = 0; l < layer_config->filter_height; ++l) {
                const int ii =
                    CLAMPINDEX(hh + l - filter_height_half, in_height);
                for (int m = 0; m < layer_config->filter_width;
                     ++m, off += cstep) {
                  const int jj =
                      CLAMPINDEX(ww + m - filter_width_half, in_width);
                  assert(ii >= 0 && ii < in_height && jj >= 0 && jj < in_width);
                  sum += layer_config->weights[off] *
                         input[k][ii * in_stride + jj];
                }
              }
            }
            const float a = sum;
            if (h == hh && w == ww)
              output[i][u * out_stride + v] = a;
            else
              output[i][u * out_stride + v] =
                  AOMMAX(output[i][u * out_stride + v], a);
          }
        }
      }
    }
  }
}

// CNNConvolve specific to maxpool set as 1, either skip_width or skip_height
// greater than 1 and padding equal to PADDING_VALID.
static void convolve_maxpool_padding_valid(
    const float **input, int in_width, int in_height, int in_stride,
    const CNN_LAYER_CONFIG *const layer_config, float **output, int out_stride,
    const int cstep) {
  for (int i = 0; i < layer_config->out_channels; ++i) {
    for (int h = 0, u = 0; h < in_height - layer_config->filter_height + 1;
         h += layer_config->skip_height, ++u) {
      for (int w = 0, v = 0; w < in_width - layer_config->filter_width + 1;
           w += layer_config->skip_width, ++v) {
        for (int hh = h; hh < AOMMIN(in_height, h + layer_config->skip_height);
             ++hh) {
          for (int ww = w; ww < AOMMIN(in_width, w + layer_config->skip_width);
               ++ww) {
            float sum = layer_config->bias[i];
            for (int k = 0; k < layer_config->in_channels; ++k) {
              int off = k * layer_config->out_channels + i;
              for (int l = 0; l < layer_config->filter_height; ++l) {
                const int ii = hh + l;
                for (int m = 0; m < layer_config->filter_width;
                     ++m, off += cstep) {
                  const int jj = ww + m;
                  assert(ii >= 0 && ii < in_height && jj >= 0 && jj < in_width);
                  sum += layer_config->weights[off] *
                         input[k][ii * in_stride + jj];
                }
              }
            }
            const float a = sum;
            if (h == hh && w == ww)
              output[i][u * out_stride + v] = a;
            else
              output[i][u * out_stride + v] =
                  AOMMAX(output[i][u * out_stride + v], a);
          }
        }
      }
    }
  }
}

// CNNConvolve specific to maxpool set as 0 with filter_height and filter_width
// equal to 1.
static void convolve_element_wise(const float **input, int in_width,
                                  int in_height, int in_stride,
                                  const CNN_LAYER_CONFIG *const layer_config,
                                  float **output, int out_stride, int start_idx,
                                  int step) {
  const int start_h = get_start_shift_convolve(
      in_height, layer_config->filter_height, layer_config->skip_height);
  const int start_w =
      get_start_shift_convolve(in_width, layer_config->filter_width,
                               layer_config->skip_width) +
      start_idx * layer_config->skip_width;
  const int out_w_step = AOMMAX(step, 1);
  const int in_w_step = layer_config->skip_width * out_w_step;
  for (int i = 0; i < layer_config->out_channels; ++i) {
    for (int h = start_h, u = 0; h < in_height;
         h += layer_config->skip_height, ++u) {
      const int in_h = h * in_stride;
      const int out_h = u * out_stride + start_idx;
      for (int w = start_w, out_index = out_h; w < in_width;
           w += in_w_step, out_index += out_w_step) {
        float sum = layer_config->bias[i];
        for (int k = 0; k < layer_config->in_channels; ++k) {
          sum += layer_config->weights[k * layer_config->out_channels + i] *
                 input[k][in_h + w];
        }
        output[i][out_index] = sum;
      }
    }
  }
}

// CNNConvolve specific to maxpool set as 0 and padding equal to
// PADDING_SAME_ZERO.
static void convolve_no_maxpool_padding_zero(
    const float **input, int in_width, int in_height, int in_stride,
    const CNN_LAYER_CONFIG *const layer_config, float **output, int out_stride,
    int start_idx, const int cstep, const int filter_width_half,
    const int filter_height_half, const int ii_shift, const int jj_shift,
    const int channel_step) {
  const int start_h = get_start_shift_convolve(
      in_height, layer_config->filter_height, layer_config->skip_height);
  const int start_w = get_start_shift_convolve(
      in_width, layer_config->filter_width, layer_config->skip_width);
  const int end_ii_shift = filter_height_half + 1;
  const int end_jj_shift = filter_width_half + 1;
  // *_filter_margin stores the number of pixels along a dimension in the
  // intersection of the complement of the image in the extended image
  // and the filter.
  const int top_filter_margin = layer_config->filter_width * ii_shift;
  const int right_filter_margin = end_jj_shift - in_width;
  for (int i = start_idx; i < layer_config->out_channels; i += channel_step) {
    for (int h = start_h, u = 0; h < in_height;
         h += layer_config->skip_height, ++u) {
      const int out_h = u * out_stride;
      const int top_cstep =
          AOMMAX(0, top_filter_margin - h * layer_config->filter_width) *
              cstep +
          i;
      const int start_ii = AOMMAX(0, h - ii_shift);
      const int end_ii = AOMMIN(in_height, h + end_ii_shift);
      for (int w = start_w, out_index = out_h; w < in_width;
           w += layer_config->skip_width, ++out_index) {
        const int left_cstep = AOMMAX(0, jj_shift - w) * cstep;
        const int right_cstep = AOMMAX(0, right_filter_margin + w) * cstep;
        const int start_jj = AOMMAX(0, w - jj_shift);
        const int end_jj = AOMMIN(in_width, w + end_jj_shift);
        float sum = layer_config->bias[i];
        for (int k = 0; k < layer_config->in_channels; ++k) {
          int off = k * layer_config->out_channels + top_cstep;
          for (int ii = start_ii; ii < end_ii; ++ii) {
            off += left_cstep;
            for (int jj = start_jj; jj < end_jj; ++jj, off += cstep) {
              sum += layer_config->weights[off] * input[k][ii * in_stride + jj];
            }
            off += right_cstep;
          }
        }
        output[i][out_index] = sum;
      }
    }
  }
}

// CNNConvolve specific to maxpool set as 0 and padding equal to
// PADDING_SAME_REPLICATE.
static void convolve_no_maxpool_padding_replicate(
    const float **input, int in_width, int in_height, int in_stride,
    const CNN_LAYER_CONFIG *const layer_config, float **output, int out_stride,
    int start_idx, const int cstep, const int ii_shift, const int jj_shift,
    const int channel_step) {
  // h and w are shifted to an offset coordinate system to reduce in-loop
  // computation.
  const int start_h =
      get_start_shift_convolve(in_height, layer_config->filter_height,
                               layer_config->skip_height) -
      ii_shift;
  const int start_w =
      get_start_shift_convolve(in_width, layer_config->filter_width,
                               layer_config->skip_width) -
      jj_shift;
  const int end_h = in_height - ii_shift;
  const int end_w = in_width - jj_shift;
  for (int i = start_idx; i < layer_config->out_channels; i += channel_step) {
    for (int h = start_h, u = 0; h < end_h;
         h += layer_config->skip_height, ++u) {
      const int out_h = u * out_stride;
      const int upper_ii_index = layer_config->filter_height + h;
      for (int w = start_w, out_index = out_h; w < end_w;
           w += layer_config->skip_width, ++out_index) {
        const int upper_jj_index = layer_config->filter_width + w;
        float sum = layer_config->bias[i];
        for (int k = 0; k < layer_config->in_channels; ++k) {
          int off = k * layer_config->out_channels + i;
          for (int ii = h; ii < upper_ii_index; ++ii) {
            const int clamped_ii = CLAMPINDEX(ii, in_height);
            for (int jj = w; jj < upper_jj_index; ++jj) {
              const int clamped_jj = CLAMPINDEX(jj, in_width);
              assert(clamped_ii >= 0 && clamped_ii < in_height &&
                     clamped_jj >= 0 && clamped_jj < in_width);
              sum += layer_config->weights[off] *
                     input[k][clamped_ii * in_stride + clamped_jj];
              off += cstep;
            }
          }
        }
        output[i][out_index] = sum;
      }
    }
  }
}

// CNNConvolve specific to maxpool set as 0 and padding equal to
// PADDING_VALID.
void av1_cnn_convolve_no_maxpool_padding_valid_c(
    const float **input, int in_width, int in_height, int in_stride,
    const CNN_LAYER_CONFIG *layer_config, float **output, int out_stride,
    int start_idx, int cstep, int channel_step) {
  assert((layer_config->skip_height == 1 && layer_config->skip_width == 1) ||
         !layer_config->maxpool);
  assert(layer_config->filter_height > 1 || layer_config->filter_width > 1);
  assert(layer_config->pad == PADDING_VALID);
  for (int i = start_idx; i < layer_config->out_channels; i += channel_step) {
    for (int h = 0, u = 0; h < in_height - layer_config->filter_height + 1;
         h += layer_config->skip_height, ++u) {
      const int out_h = u * out_stride;
      const int upper_ii_index = layer_config->filter_height + h;
      for (int w = 0, out_index = out_h;
           w < in_width - layer_config->filter_width + 1;
           w += layer_config->skip_width, ++out_index) {
        const int upper_jj_index = layer_config->filter_width + w;
        float sum = layer_config->bias[i];
        for (int k = 0; k < layer_config->in_channels; ++k) {
          int off = k * layer_config->out_channels + i;
          for (int ii = h; ii < upper_ii_index; ++ii) {
            for (int jj = w; jj < upper_jj_index; ++jj) {
              assert(ii >= 0 && ii < in_height && jj >= 0 && jj < in_width);
              sum += layer_config->weights[off] * input[k][ii * in_stride + jj];
              off += cstep;
            }
          }
        }
        output[i][out_index] = sum;
      }
    }
  }
}

static void av1_cnn_convolve(const float **input, int in_width, int in_height,
                             int in_stride,
                             const CNN_LAYER_CONFIG *layer_config,
                             float **output, int out_stride, int start_idx,
                             int step) {
  assert(!layer_config->deconvolve);
  const int cstep = layer_config->in_channels * layer_config->out_channels;
  const int filter_height_half = layer_config->filter_height >> 1;
  const int filter_width_half = layer_config->filter_width >> 1;
  const int channel_step = AOMMAX(step, 1);

  if (layer_config->maxpool &&
      (layer_config->skip_height > 1 || layer_config->skip_width > 1)) {
    switch (layer_config->pad) {
      case PADDING_SAME_ZERO:
        convolve_maxpool_padding_zero(input, in_width, in_height, in_stride,
                                      layer_config, output, out_stride, cstep,
                                      filter_width_half, filter_height_half);
        break;
      case PADDING_SAME_REPLICATE:
        convolve_maxpool_padding_replicate(
            input, in_width, in_height, in_stride, layer_config, output,
            out_stride, cstep, filter_width_half, filter_height_half);
        break;
      case PADDING_VALID:
        convolve_maxpool_padding_valid(input, in_width, in_height, in_stride,
                                       layer_config, output, out_stride, cstep);
        break;
      default: assert(0 && "Unknown padding type");
    }
  } else {
    // Results in element-wise matrix multiplication.
    if (layer_config->filter_height == 1 && layer_config->filter_width == 1) {
      convolve_element_wise(input, in_width, in_height, in_stride, layer_config,
                            output, out_stride, start_idx, step);
      return;
    }
    const int ii_shift =
        filter_height_half - (layer_config->filter_height - 1) % 2;
    const int jj_shift =
        filter_width_half - (layer_config->filter_width - 1) % 2;
    switch (layer_config->pad) {
      case PADDING_SAME_ZERO:
        convolve_no_maxpool_padding_zero(
            input, in_width, in_height, in_stride, layer_config, output,
            out_stride, start_idx, cstep, filter_width_half, filter_height_half,
            ii_shift, jj_shift, channel_step);
        break;
      case PADDING_SAME_REPLICATE:
        convolve_no_maxpool_padding_replicate(
            input, in_width, in_height, in_stride, layer_config, output,
            out_stride, start_idx, cstep, ii_shift, jj_shift, channel_step);
        break;
      case PADDING_VALID:
        av1_cnn_convolve_no_maxpool_padding_valid(
            input, in_width, in_height, in_stride, layer_config, output,
            out_stride, start_idx, cstep, channel_step);
        break;
      default: assert(0 && "Unknown padding type");
    }
  }
}

static int convolve_layer(void *arg1, void *arg2) {
  const CONVOLVE_OPS *convolve_ops = arg1;
  (void)arg2;
  av1_cnn_convolve(
      convolve_ops->input, convolve_ops->in_width, convolve_ops->in_height,
      convolve_ops->in_stride, convolve_ops->layer_config, convolve_ops->output,
      convolve_ops->out_stride, convolve_ops->start_idx, convolve_ops->th_step);
  return 1;
}

static void convolve_layer_mt(const float **input, int in_width, int in_height,
                              int in_stride,
                              const CNN_LAYER_CONFIG *layer_config,
                              const CNN_THREAD_DATA *thread_data,
                              float **output, int out_stride) {
  const AVxWorkerInterface *const winterface = aom_get_worker_interface();
  const int num_workers = thread_data->num_workers;

  CONVOLVE_OPS convolve_ops[CNN_MAX_THREADS];
  for (int th = 0; th < AOMMIN(num_workers, CNN_MAX_THREADS); ++th) {
    AVxWorker *const worker = &thread_data->workers[th];
    winterface->reset(worker);

    CONVOLVE_OPS convolve_op = { input,      in_width,     in_height,
                                 in_stride,  layer_config, output,
                                 out_stride, th,           num_workers };
    convolve_ops[th] = convolve_op;
    worker->hook = convolve_layer;
    worker->data1 = &(convolve_ops[th]);
    worker->data2 = NULL;

    // Start convolving.
    if (th == num_workers - 1) {
      winterface->execute(worker);
    } else {
      winterface->launch(worker);
    }
  }

  // Wait until all workers have finished.
  for (int th = 0; th < AOMMIN(num_workers, CNN_MAX_THREADS); ++th) {
    winterface->sync(&thread_data->workers[th]);
  }
}

static INLINE int get_start_shift_deconvolve(int filt_width, int stride) {
  const int dif = AOMMAX(filt_width - stride, 0);
  return dif / 2;
}

void av1_cnn_batchnorm_c(float **image, int channels, int width, int height,
                         int stride, const float *gamma, const float *beta,
                         const float *mean, const float *std) {
  assert(gamma && beta && beta && std && "batchnorm has null parameter!");
  for (int ch = 0; ch < channels; ch++) {
    const float ch_gamma = gamma[ch];
    const float ch_beta = beta[ch];
    const float ch_mean = mean[ch];
    const float ch_std = std[ch];
    float *image_row = image[ch];

    for (int row = 0; row < height; row++) {
      for (int col = 0; col < width; col++) {
        image_row[col] =
            ch_gamma * (image_row[col] - ch_mean) / ch_std + ch_beta;
      }
      image_row += stride;
    }
  }
}

void av1_cnn_deconvolve_c(const float **input, int in_width, int in_height,
                          int in_stride, const CNN_LAYER_CONFIG *layer_config,
                          float **output, int out_stride) {
  assert(layer_config->deconvolve);

  const int cstep = layer_config->in_channels * layer_config->out_channels;

  int out_width = 0;
  int out_height = 0;
  av1_find_cnn_layer_output_size(in_width, in_height, layer_config, &out_width,
                                 &out_height);
  switch (layer_config->pad) {
    case PADDING_SAME_ZERO:
      for (int i = 0; i < layer_config->out_channels; ++i) {
        for (int u = 0; u < out_height; ++u) {
          for (int v = 0; v < out_width; ++v) {
            float sum = layer_config->bias[i];
            for (int k = 0; k < layer_config->in_channels; ++k) {
              int off = k * layer_config->out_channels + i;
              for (int l = 0; l < layer_config->filter_height; ++l) {
                const int h =
                    u - l +
                    get_start_shift_deconvolve(layer_config->filter_height,
                                               layer_config->skip_height);
                for (int m = 0; m < layer_config->filter_width;
                     ++m, off += cstep) {
                  const int w =
                      v - m +
                      get_start_shift_deconvolve(layer_config->filter_width,
                                                 layer_config->skip_width);
                  if ((h % layer_config->skip_height) != 0 ||
                      (w % layer_config->skip_width) != 0)
                    continue;
                  const int ii = h / layer_config->skip_height;
                  const int jj = w / layer_config->skip_width;
                  if (ii < 0 || ii >= in_height || jj < 0 || jj >= in_width)
                    continue;
                  sum += layer_config->weights[off] *
                         input[k][ii * in_stride + jj];
                }
              }
            }
            output[i][u * out_stride + v] = sum;
          }
        }
      }
      break;
    case PADDING_SAME_REPLICATE:
      for (int i = 0; i < layer_config->out_channels; ++i) {
        for (int u = 0; u < out_height; ++u) {
          for (int v = 0; v < out_width; ++v) {
            float sum = layer_config->bias[i];
            for (int k = 0; k < layer_config->in_channels; ++k) {
              int off = k * layer_config->out_channels + i;
              for (int l = 0; l < layer_config->filter_height; ++l) {
                const int h =
                    u - l +
                    get_start_shift_deconvolve(layer_config->filter_height,
                                               layer_config->skip_height);
                for (int m = 0; m < layer_config->filter_width;
                     ++m, off += cstep) {
                  const int w =
                      v - m +
                      get_start_shift_deconvolve(layer_config->filter_width,
                                                 layer_config->skip_width);
                  if ((h % layer_config->skip_height) != 0 ||
                      (w % layer_config->skip_width) != 0)
                    continue;
                  const int ii =
                      CLAMPINDEX(h / layer_config->skip_height, in_height);
                  const int jj =
                      CLAMPINDEX(w / layer_config->skip_width, in_width);
                  assert(ii >= 0 && ii < in_height && jj >= 0 && jj < in_width);
                  sum += layer_config->weights[off] *
                         input[k][ii * in_stride + jj];
                }
              }
            }
            output[i][u * out_stride + v] = sum;
          }
        }
      }
      break;
    case PADDING_VALID:
      for (int i = 0; i < layer_config->out_channels; ++i) {
        for (int u = 0; u < out_height; ++u) {
          for (int v = 0; v < out_width; ++v) {
            float sum = layer_config->bias[i];
            for (int k = 0; k < layer_config->in_channels; ++k) {
              int off = k * layer_config->out_channels + i;
              for (int l = 0; l < layer_config->filter_height; ++l) {
                const int h = u - l;
                for (int m = 0; m < layer_config->filter_width;
                     ++m, off += cstep) {
                  const int w = v - m;
                  if ((h % layer_config->skip_height) != 0 ||
                      (w % layer_config->skip_width) != 0)
                    continue;
                  const int ii = h / layer_config->skip_height;
                  const int jj = w / layer_config->skip_width;
                  if (ii < 0 || ii >= in_height || jj < 0 || jj >= in_width)
                    continue;
                  sum += layer_config->weights[off] *
                         input[k][ii * in_stride + jj];
                }
              }
            }
            output[i][u * out_stride + v] = sum;
          }
        }
      }
      break;
    default: assert(0 && "Unknown padding type");
  }
}

void av1_cnn_predict_c(const float **input, int in_width, int in_height,
                       int in_stride, const CNN_CONFIG *cnn_config,
                       const CNN_THREAD_DATA *thread_data,
                       CNN_MULTI_OUT *output_struct) {
  TENSOR tensor1[CNN_MAX_BRANCHES] = { { 0 } };
  TENSOR tensor2[CNN_MAX_BRANCHES] = { { 0 } };

  float **output[CNN_MAX_BRANCHES];
  const int *out_chs = output_struct->output_channels;
  output[0] = output_struct->output_buffer;
  for (int out_idx = 1; out_idx < output_struct->num_outputs; out_idx++) {
    output[out_idx] = output[out_idx - 1] + out_chs[out_idx - 1];
  }

  int i_width = in_width;
  int i_height = in_height;
  int o_width = 0, o_height = 0;
  for (int b = 0; b < CNN_MAX_BRANCHES; ++b) {
    init_tensor(&tensor1[b]);
    init_tensor(&tensor2[b]);
  }

  const int *out_stride = output_struct->output_strides;
  for (int layer = 0; layer < cnn_config->num_layers; ++layer) {
    const CNN_LAYER_CONFIG *layer_config = &cnn_config->layer_config[layer];
    const int branch = layer_config->branch;
    const CNN_BRANCH_CONFIG *branch_config = &layer_config->branch_config;

    // Allocate input tensor
    if (layer == 0) {       // First layer
      assert(branch == 0);  // First layer must be primary branch
      assign_tensor(&tensor1[branch], (float **)input,
                    layer_config->in_channels, in_width, in_height, in_stride);
    } else {  // Non-first layer
      // Swap tensor1 and tensor2
      swap_tensor(&tensor1[branch], &tensor2[branch]);

      i_width = tensor1[branch].width;
      i_height = tensor1[branch].height;
    }

    // Allocate output tensor
    av1_find_cnn_layer_output_size(i_width, i_height, layer_config, &o_width,
                                   &o_height);
    const int output_num = layer_config->output_num;
    if (output_num == -1) {  // Non-output layer
      realloc_tensor(&tensor2[branch], layer_config->out_channels, o_width,
                     o_height);
    } else {  // Output layer
      free_tensor(&tensor2[branch]);
      assign_tensor(&tensor2[branch], output[output_num],
                    layer_config->out_channels, o_width, o_height,
                    out_stride[output_num]);
    }

    // If we are combining branches make sure that the branch to combine
    // is different from the current branch.
    assert(IMPLIES(layer_config->branch_combine_type != BRANCH_NOC,
                   !(branch_config->branches_to_combine & (1 << branch))));

    if (layer_config->branch_copy_type == BRANCH_INPUT) {
      copy_active_tensor_to_branches(&tensor1[branch], layer_config, branch,
                                     tensor2);
    }
    // Check consistency of input and output channels
    assert(tensor1[branch].channels == layer_config->in_channels);
    assert(tensor2[branch].channels == layer_config->out_channels);

    // Convolve/Deconvolve
    if (!cnn_config->layer_config[layer].deconvolve) {
      if (thread_data->num_workers > 1) {
        convolve_layer_mt((const float **)tensor1[branch].buf,
                          tensor1[branch].width, tensor1[branch].height,
                          tensor1[branch].stride, layer_config, thread_data,
                          tensor2[branch].buf, tensor2[branch].stride);
      } else {
        av1_cnn_convolve((const float **)tensor1[branch].buf,
                         tensor1[branch].width, tensor1[branch].height,
                         tensor1[branch].stride, layer_config,
                         tensor2[branch].buf, tensor2[branch].stride, 0, 1);
      }
    } else {
      av1_cnn_deconvolve((const float **)tensor1[branch].buf,
                         tensor1[branch].width, tensor1[branch].height,
                         tensor1[branch].stride, layer_config,
                         tensor2[branch].buf, tensor2[branch].stride);
    }

    if (layer_config->branch_copy_type == BRANCH_OUTPUT) {
      copy_active_tensor_to_branches(&tensor2[branch], layer_config, branch,
                                     tensor2);
    }

    // Add tensors from other branches if needed
    if (layer_config->branch_combine_type == BRANCH_ADD) {
      for (int b = 0; b < CNN_MAX_BRANCHES; ++b) {
        if ((branch_config->branches_to_combine & (1 << b)) && b != branch) {
          assert(check_tensor_equal_size(&tensor2[b], &tensor2[branch]));
          av1_cnn_add(tensor2[branch].buf, tensor2[branch].channels,
                      tensor2[branch].width, tensor2[branch].height,
                      tensor2[branch].stride, (const float **)tensor2[b].buf);
        }
      }
    }

    // Non-linearity
    if (layer_config->activation != IDENTITY)
      av1_cnn_activate(tensor2[branch].buf, tensor2[branch].channels,
                       tensor2[branch].width, tensor2[branch].height,
                       tensor2[branch].stride, layer_config->activation);

    if (layer_config->bn_params.bn_gamma) {
      av1_cnn_batchnorm(
          tensor2[branch].buf, tensor2[branch].channels, tensor2[branch].width,
          tensor2[branch].height, tensor2[branch].stride,
          layer_config->bn_params.bn_gamma, layer_config->bn_params.bn_beta,
          layer_config->bn_params.bn_mean, layer_config->bn_params.bn_std);
    }

    // Concatenate tensors
    if (layer_config->branch_combine_type == BRANCH_CAT) {
      if (output_num == -1) {  // Non-output layer
        for (int b = 0; b < CNN_MAX_BRANCHES; ++b) {
          if ((branch_config->branches_to_combine & (1 << b)) && b != branch) {
            assert(check_tensor_equal_dims(&tensor2[b], &tensor2[branch]));
            assert(tensor2[b].channels > 0);
            concat_tensor(&tensor2[b], &tensor2[branch]);
          }
        }
      } else {  // Output layer
        const int existing_channels = tensor2[branch].channels;
        int num_chs = existing_channels;
        for (int b = 0; b < CNN_MAX_BRANCHES; ++b) {
          if ((branch_config->branches_to_combine & (1 << b)) && b != branch) {
            assert(check_tensor_equal_dims(&tensor2[b], &tensor2[branch]));
            // Needed only to assign the new channel buffers
            num_chs += tensor2[b].channels;
          }
        }
        assign_tensor(&tensor2[branch], output[output_num], num_chs, o_width,
                      o_height, out_stride[output_num]);

        num_chs = existing_channels;
        for (int b = 0; b < CNN_MAX_BRANCHES; ++b) {
          if ((branch_config->branches_to_combine & (1 << b)) && b != branch) {
            assert(check_tensor_equal_dims(&tensor2[b], &tensor2[branch]));
            // Needed only to assign the new channel buffers
            copy_tensor(&tensor2[b], tensor2[b].channels, num_chs,
                        &tensor2[branch]);
            num_chs += tensor2[b].channels;
          }
        }
      }
    }

    if (layer_config->branch_copy_type == BRANCH_COMBINED) {
      copy_active_tensor_to_branches(&tensor2[branch], layer_config, branch,
                                     tensor2);
    }
  }

  for (int b = 0; b < CNN_MAX_BRANCHES; ++b) {
    free_tensor(&tensor1[b]);
    free_tensor(&tensor2[b]);
  }
}

// Assume output already has proper allocation
// Assume input image buffers all have same resolution and strides
void av1_cnn_predict_img_multi_out(uint8_t **dgd, int width, int height,
                                   int stride, const CNN_CONFIG *cnn_config,
                                   const CNN_THREAD_DATA *thread_data,
                                   CNN_MULTI_OUT *output) {
  const float max_val = 255.0;

  const int in_width = width + 2 * cnn_config->ext_width;
  const int in_height = height + 2 * cnn_config->ext_height;
  const int in_channels = cnn_config->layer_config[0].in_channels;
  float *inputs[CNN_MAX_CHANNELS];
  float *input_ =
      (float *)aom_malloc(in_width * in_height * in_channels * sizeof(*input_));
  const int in_stride = in_width;

  for (int c = 0; c < in_channels; ++c) {
    inputs[c] = input_ + c * in_stride * in_height;
    float *input =
        inputs[c] + cnn_config->ext_height * in_stride + cnn_config->ext_width;

    if (cnn_config->strict_bounds) {
      for (int i = 0; i < height; ++i)
        for (int j = 0; j < width; ++j)
          input[i * in_stride + j] = (float)dgd[c][i * stride + j] / max_val;
      // extend left and right
      for (int i = 0; i < height; ++i) {
        for (int j = -cnn_config->ext_width; j < 0; ++j)
          input[i * in_stride + j] = input[i * in_stride];
        for (int j = width; j < width + cnn_config->ext_width; ++j)
          input[i * in_stride + j] = input[i * in_stride + width - 1];
      }
      // extend top and bottom
      for (int i = -cnn_config->ext_height; i < 0; ++i)
        memcpy(&input[i * in_stride - cnn_config->ext_width],
               &input[-cnn_config->ext_width], in_width * sizeof(*input));
      for (int i = height; i < height + cnn_config->ext_height; ++i)
        memcpy(&input[i * in_stride - cnn_config->ext_width],
               &input[(height - 1) * in_stride - cnn_config->ext_width],
               in_width * sizeof(*input));
    } else {
      for (int i = -cnn_config->ext_height; i < height + cnn_config->ext_height;
           ++i)
        for (int j = -cnn_config->ext_width; j < width + cnn_config->ext_width;
             ++j)
          input[i * in_stride + j] = (float)dgd[c][i * stride + j] / max_val;
    }
  }
  av1_cnn_predict((const float **)inputs, in_width, in_height, in_stride,
                  cnn_config, thread_data, output);

  aom_free(input_);
}

// Assume output already has proper allocation
// Assume input image buffers all have same resolution and strides
void av1_cnn_predict_img_multi_out_highbd(uint16_t **dgd, int width, int height,
                                          int stride,
                                          const CNN_CONFIG *cnn_config,
                                          const CNN_THREAD_DATA *thread_data,
                                          int bit_depth,
                                          CNN_MULTI_OUT *output) {
  const float max_val = (float)((1 << bit_depth) - 1);

  const int in_width = width + 2 * cnn_config->ext_width;
  const int in_height = height + 2 * cnn_config->ext_height;
  const int in_channels = cnn_config->layer_config[0].in_channels;
  float *inputs[CNN_MAX_CHANNELS];
  float *input_ =
      (float *)aom_malloc(in_width * in_height * in_channels * sizeof(*input_));
  const int in_stride = in_width;

  for (int c = 0; c < in_channels; ++c) {
    inputs[c] = input_ + c * in_stride * in_height;
    float *input =
        inputs[c] + cnn_config->ext_height * in_stride + cnn_config->ext_width;

    if (cnn_config->strict_bounds) {
      for (int i = 0; i < height; ++i)
        for (int j = 0; j < width; ++j)
          input[i * in_stride + j] = (float)dgd[c][i * stride + j] / max_val;
      // extend left and right
      for (int i = 0; i < height; ++i) {
        for (int j = -cnn_config->ext_width; j < 0; ++j)
          input[i * in_stride + j] = input[i * in_stride];
        for (int j = width; j < width + cnn_config->ext_width; ++j)
          input[i * in_stride + j] = input[i * in_stride + width - 1];
      }
      // extend top and bottom
      for (int i = -cnn_config->ext_height; i < 0; ++i)
        memcpy(&input[i * in_stride - cnn_config->ext_width],
               &input[-cnn_config->ext_width], in_width * sizeof(*input));
      for (int i = height; i < height + cnn_config->ext_height; ++i)
        memcpy(&input[i * in_stride - cnn_config->ext_width],
               &input[(height - 1) * in_stride - cnn_config->ext_width],
               in_width * sizeof(*input));
    } else {
      for (int i = -cnn_config->ext_height; i < height + cnn_config->ext_height;
           ++i)
        for (int j = -cnn_config->ext_width; j < width + cnn_config->ext_width;
             ++j)
          input[i * in_stride + j] = (float)dgd[c][i * stride + j] / max_val;
    }
  }

  av1_cnn_predict((const float **)inputs, in_width, in_height, in_stride,
                  cnn_config, thread_data, output);

  aom_free(input_);
}

// Assume output already has proper allocation
// Assume input image buffers all have same resolution and strides
void av1_cnn_predict_img(uint8_t **dgd, int width, int height, int stride,
                         const CNN_CONFIG *cnn_config,
                         const CNN_THREAD_DATA *thread_data, float **output,
                         int out_stride) {
  int out_width = 0, out_height = 0, out_channels = 0;
  av1_find_cnn_output_size(width, height, cnn_config, &out_width, &out_height,
                           &out_channels);
  const int output_chs[1] = { out_channels };
  const int output_strides[1] = { out_stride };
  CNN_MULTI_OUT output_struct = { .output_channels = output_chs,
                                  .output_strides = output_strides,
                                  .output_buffer = output };
  av1_cnn_predict_img_multi_out(dgd, width, height, stride, cnn_config,
                                thread_data, &output_struct);
}

// Assume output already has proper allocation
// Assume input image buffers all have same resolution and strides
void av1_cnn_predict_img_highbd(uint16_t **dgd, int width, int height,
                                int stride, const CNN_CONFIG *cnn_config,
                                const CNN_THREAD_DATA *thread_data,
                                int bit_depth, float **output, int out_stride) {
  int out_width = 0, out_height = 0, out_channels = 0;
  av1_find_cnn_output_size(width, height, cnn_config, &out_width, &out_height,
                           &out_channels);
  const int output_chs[1] = { out_channels };
  const int output_strides[1] = { out_stride };
  CNN_MULTI_OUT output_struct = { .output_channels = output_chs,
                                  .output_strides = output_strides,
                                  .output_buffer = output };
  av1_cnn_predict_img_multi_out_highbd(dgd, width, height, stride, cnn_config,
                                       thread_data, bit_depth, &output_struct);
}
