/**
 * Source: https://github.com/woltapp/blurhash
 *
 * Copyright (c) 2018 Wolt Enterprises
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute,
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
 * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package org.signal.blurhash;

import android.graphics.Bitmap;
import android.graphics.Color;

import androidx.annotation.Nullable;

import static org.signal.blurhash.BlurHashUtil.linearTosRGB;
import static org.signal.blurhash.BlurHashUtil.sRGBToLinear;
import static org.signal.blurhash.BlurHashUtil.signPow;

public class BlurHashDecoder {

  public static @Nullable Bitmap decode(@Nullable String blurHash, int width, int height) {
    return decode(blurHash, width, height, 1f);
  }

  static @Nullable Bitmap decode(@Nullable String blurHash, int width, int height, double punch) {

    if (blurHash == null || blurHash.length() < 6) {
      return null;
    }

    int numCompEnc = Base83.decode(blurHash, 0, 1);
    int numCompX   = (numCompEnc % 9) + 1;
    int numCompY   = (numCompEnc / 9) + 1;

    if (blurHash.length() != 4 + 2 * numCompX * numCompY) {
      return null;
    }

    int        maxAcEnc = Base83.decode(blurHash, 1, 2);
    double     maxAc    = (maxAcEnc + 1) / 166f;
    double[][] colors   = new double[numCompX * numCompY][];
    for (int i = 0; i < colors.length; i++) {
      if (i == 0) {
        int colorEnc = Base83.decode(blurHash, 2, 6);
        colors[i] = decodeDc(colorEnc);
      } else {
        int from = 4 + i * 2;
        int colorEnc = Base83.decode(blurHash, from, from + 2);
        colors[i] = decodeAc(colorEnc, maxAc * punch);
      }
    }

    return composeBitmap(width, height, numCompX, numCompY, colors);
  }

  private static double[] decodeDc(int colorEnc) {
    int r = colorEnc >> 16;
    int g = (colorEnc >> 8) & 255;
    int b = colorEnc & 255;
    return new double[] {sRGBToLinear(r),
                         sRGBToLinear(g),
                         sRGBToLinear(b)};
  }

  private static double[] decodeAc(int value, double maxAc) {
    int r = value / (19 * 19);
    int g = (value / 19) % 19;
    int b = value % 19;
    return new double[]{
        signPow((r - 9) / 9.0f, 2f) * maxAc,
        signPow((g - 9) / 9.0f, 2f) * maxAc,
        signPow((b - 9) / 9.0f, 2f) * maxAc
    };
  }

  private static Bitmap composeBitmap(int width, int height, int numCompX, int numCompY, double[][] colors) {
    Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    for (int y = 0; y < height; y++) {
      for (int x = 0; x < width; x++) {

        double r = 0f;
        double g = 0f;
        double b = 0f;

        for (int j = 0; j < numCompY; j++) {
          for (int i = 0; i < numCompX; i++) {
            double basis = (Math.cos(Math.PI * x * i / width) * Math.cos(Math.PI * y * j / height));
            double[] color = colors[j * numCompX + i];
            r += color[0] * basis;
            g += color[1] * basis;
            b += color[2] * basis;
          }
        }
        bitmap.setPixel(x, y, Color.rgb((int) linearTosRGB(r), (int) linearTosRGB(g), (int) linearTosRGB(b)));
      }
    }

    return bitmap;
  }
}
