/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.gfx;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;

import org.mozilla.gecko.R;
import org.mozilla.gecko.util.GeckoJarReader;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.UIAsyncTask;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;

public final class BitmapUtils {
    /* Default colors. */
    private static final float[] DEFAULT_LAUNCHER_ICON_HSV = { 32.0f, 1.0f, 1.0f };

    private static final String LOGTAG = "GeckoBitmapUtils";

    private BitmapUtils() {}

    public interface BitmapLoader {
        public void onBitmapFound(Drawable d);
    }

    public static void runOnBitmapFoundOnUiThread(final BitmapLoader loader, final Drawable d) {
        if (ThreadUtils.isOnUiThread()) {
            loader.onBitmapFound(d);
            return;
        }

        ThreadUtils.postToUiThread(new Runnable() {
            @Override
            public void run() {
                loader.onBitmapFound(d);
            }
        });
    }

    /**
     * Attempts to find a drawable associated with a given string, using its URI scheme to determine
     * how to load the drawable. The BitmapLoader's `onBitmapFound` method is always called, and
     * will be called with `null` if no drawable is found.
     *
     * The BitmapLoader `onBitmapFound` method always runs on the UI thread.
     */
    public static void getDrawable(final Context context, final String data, final BitmapLoader loader) {
        if (TextUtils.isEmpty(data)) {
            runOnBitmapFoundOnUiThread(loader, null);
            return;
        }

        if (data.startsWith("data")) {
            final BitmapDrawable d = new BitmapDrawable(context.getResources(), getBitmapFromDataURI(data));
            runOnBitmapFoundOnUiThread(loader, d);
            return;
        }

        if (data.startsWith("jar:") || data.startsWith("file://")) {
            (new UIAsyncTask.WithoutParams<Drawable>(ThreadUtils.getBackgroundHandler()) {
                @Override
                public Drawable doInBackground() {
                    try {
                        if (data.startsWith("jar:jar")) {
                            return GeckoJarReader.getBitmapDrawable(
                                    context, context.getResources(), data);
                        }

                        // Don't attempt to validate the JAR signature when loading an add-on icon
                        if (data.startsWith("jar:file")) {
                            return GeckoJarReader.getBitmapDrawable(
                                    context, context.getResources(), Uri.decode(data));
                        }

                        final URL url = new URL(data);
                        final InputStream is = (InputStream) url.getContent();
                        try {
                            return Drawable.createFromStream(is, "src");
                        } finally {
                            is.close();
                        }
                    } catch (Exception e) {
                        Log.w(LOGTAG, "Unable to set icon", e);
                    }
                    return null;
                }

                @Override
                public void onPostExecute(Drawable drawable) {
                    loader.onBitmapFound(drawable);
                }
            }).execute();
            return;
        }

        if (data.startsWith("-moz-icon://")) {
            final Uri imageUri = Uri.parse(data);
            final String ssp = imageUri.getSchemeSpecificPart();
            final String resource = ssp.substring(ssp.lastIndexOf('/') + 1);

            try {
                final Drawable d = context.getPackageManager().getApplicationIcon(resource);
                runOnBitmapFoundOnUiThread(loader, d);
            } catch (Exception ex) { }

            return;
        }

        if (data.startsWith("drawable://")) {
            final Uri imageUri = Uri.parse(data);
            final int id = getResource(imageUri, R.drawable.ic_status_logo);
            final Drawable d = context.getResources().getDrawable(id);

            runOnBitmapFoundOnUiThread(loader, d);
            return;
        }

        runOnBitmapFoundOnUiThread(loader, null);
    }

    public static Bitmap decodeByteArray(byte[] bytes) {
        return decodeByteArray(bytes, null);
    }

    public static Bitmap decodeByteArray(byte[] bytes, BitmapFactory.Options options) {
        return decodeByteArray(bytes, 0, bytes.length, options);
    }

    public static Bitmap decodeByteArray(byte[] bytes, int offset, int length) {
        return decodeByteArray(bytes, offset, length, null);
    }

    public static Bitmap decodeByteArray(byte[] bytes, int offset, int length, BitmapFactory.Options options) {
        if (bytes.length <= 0) {
            throw new IllegalArgumentException("bytes.length " + bytes.length
                                               + " must be a positive number");
        }

        Bitmap bitmap = null;
        try {
            bitmap = BitmapFactory.decodeByteArray(bytes, offset, length, options);
        } catch (OutOfMemoryError e) {
            Log.e(LOGTAG, "decodeByteArray(bytes.length=" + bytes.length
                          + ", options= " + options + ") OOM!", e);
            return null;
        }

        if (bitmap == null) {
            Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned null");
            return null;
        }

        if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
            Log.w(LOGTAG, "decodeByteArray() returning null because BitmapFactory returned "
                          + "a bitmap with dimensions " + bitmap.getWidth()
                          + "x" + bitmap.getHeight());
            return null;
        }

        return bitmap;
    }

    public static Bitmap decodeStream(InputStream inputStream) {
        try {
            return BitmapFactory.decodeStream(inputStream);
        } catch (OutOfMemoryError e) {
            Log.e(LOGTAG, "decodeStream() OOM!", e);
            return null;
        }
    }

    public static Bitmap decodeUrl(Uri uri) {
        return decodeUrl(uri.toString());
    }

    public static Bitmap decodeUrl(String urlString) {
        URL url;

        try {
            url = new URL(urlString);
        } catch (MalformedURLException e) {
            Log.w(LOGTAG, "decodeUrl: malformed URL " + urlString);
            return null;
        }

        return decodeUrl(url);
    }

    public static Bitmap decodeUrl(URL url) {
        InputStream stream = null;

        try {
            stream = url.openStream();
        } catch (IOException e) {
            Log.w(LOGTAG, "decodeUrl: IOException downloading " + url);
            return null;
        }

        if (stream == null) {
            Log.w(LOGTAG, "decodeUrl: stream not found downloading " + url);
            return null;
        }

        Bitmap bitmap = decodeStream(stream);

        try {
            stream.close();
        } catch (IOException e) {
            Log.w(LOGTAG, "decodeUrl: IOException closing stream " + url, e);
        }

        return bitmap;
    }

    public static Bitmap decodeResource(Context context, int id) {
        return decodeResource(context, id, null);
    }

    public static Bitmap decodeResource(Context context, int id, BitmapFactory.Options options) {
        Resources resources = context.getResources();
        try {
            return BitmapFactory.decodeResource(resources, id, options);
        } catch (OutOfMemoryError e) {
            Log.e(LOGTAG, "decodeResource() OOM! Resource id=" + id, e);
            return null;
        }
    }

    public static int getDominantColor(Bitmap source) {
        return getDominantColor(source, true);
    }

    public static int getDominantColor(Bitmap source, boolean applyThreshold) {
      if (source == null)
        return Color.argb(255, 255, 255, 255);

      // Keep track of how many times a hue in a given bin appears in the image.
      // Hue values range [0 .. 360), so dividing by 10, we get 36 bins.
      int[] colorBins = new int[36];

      // The bin with the most colors. Initialize to -1 to prevent accidentally
      // thinking the first bin holds the dominant color.
      int maxBin = -1;

      // Keep track of sum hue/saturation/value per hue bin, which we'll use to
      // compute an average to for the dominant color.
      float[] sumHue = new float[36];
      float[] sumSat = new float[36];
      float[] sumVal = new float[36];
      float[] hsv = new float[3];

      int height = source.getHeight();
      int width = source.getWidth();
      int[] pixels = new int[width * height];
      source.getPixels(pixels, 0, width, 0, 0, width, height);
      for (int row = 0; row < height; row++) {
        for (int col = 0; col < width; col++) {
          int c = pixels[col + row * width];
          // Ignore pixels with a certain transparency.
          if (Color.alpha(c) < 128)
            continue;

          Color.colorToHSV(c, hsv);

          // If a threshold is applied, ignore arbitrarily chosen values for "white" and "black".
          if (applyThreshold && (hsv[1] <= 0.35f || hsv[2] <= 0.35f))
            continue;

          // We compute the dominant color by putting colors in bins based on their hue.
          int bin = (int) Math.floor(hsv[0] / 10.0f);

          // Update the sum hue/saturation/value for this bin.
          sumHue[bin] = sumHue[bin] + hsv[0];
          sumSat[bin] = sumSat[bin] + hsv[1];
          sumVal[bin] = sumVal[bin] + hsv[2];

          // Increment the number of colors in this bin.
          colorBins[bin]++;

          // Keep track of the bin that holds the most colors.
          if (maxBin < 0 || colorBins[bin] > colorBins[maxBin])
            maxBin = bin;
        }
      }

      // maxBin may never get updated if the image holds only transparent and/or black/white pixels.
      if (maxBin < 0)
        return Color.argb(255, 255, 255, 255);

      // Return a color with the average hue/saturation/value of the bin with the most colors.
      hsv[0] = sumHue[maxBin] / colorBins[maxBin];
      hsv[1] = sumSat[maxBin] / colorBins[maxBin];
      hsv[2] = sumVal[maxBin] / colorBins[maxBin];
      return Color.HSVToColor(hsv);
    }

    /**
     * Decodes a bitmap from a Base64 data URI.
     *
     * @param dataURI a Base64-encoded data URI string
     * @return        the decoded bitmap, or null if the data URI is invalid
     */
    public static Bitmap getBitmapFromDataURI(String dataURI) {
        if (dataURI == null) {
            return null;
        }

        byte[] raw = getBytesFromDataURI(dataURI);
        if (raw == null || raw.length == 0) {
            return null;
        }

        return decodeByteArray(raw);
    }

    /**
     * Return a byte[] containing the bytes in a given base64 string, or null if this is not a valid
     * base64 string.
     */
    public static byte[] getBytesFromBase64(String base64) {
        try {
            return Base64.decode(base64, Base64.DEFAULT);
        } catch (Exception e) {
            Log.e(LOGTAG, "exception decoding bitmap from data URI: " + base64, e);
        }

        return null;
    }

    public static byte[] getBytesFromDataURI(String dataURI) {
        final String base64 = dataURI.substring(dataURI.indexOf(',') + 1);
        return getBytesFromBase64(base64);
    }

    public static Bitmap getBitmapFromDrawable(Drawable drawable) {
        if (drawable instanceof BitmapDrawable) {
            return ((BitmapDrawable) drawable).getBitmap();
        }

        int width = drawable.getIntrinsicWidth();
        width = width > 0 ? width : 1;
        int height = drawable.getIntrinsicHeight();
        height = height > 0 ? height : 1;

        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);

        return bitmap;
    }

    public static int getResource(Uri resourceUrl, int defaultIcon) {
        int icon = defaultIcon;

        final String scheme = resourceUrl.getScheme();
        if ("drawable".equals(scheme)) {
            String resource = resourceUrl.getSchemeSpecificPart();
            resource = resource.substring(resource.lastIndexOf('/') + 1);

            try {
                return Integer.parseInt(resource);
            } catch (NumberFormatException ex) {
                // This isn't a resource id, try looking for a string
            }

            try {
                final Class<R.drawable> drawableClass = R.drawable.class;
                final Field f = drawableClass.getField(resource);
                icon = f.getInt(null);
            } catch (final NoSuchFieldException e1) {

                // just means the resource doesn't exist for fennec. Check in Android resources
                try {
                    final Class<android.R.drawable> drawableClass = android.R.drawable.class;
                    final Field f = drawableClass.getField(resource);
                    icon = f.getInt(null);
                } catch (final NoSuchFieldException e2) {
                    // This drawable doesn't seem to exist...
                } catch (Exception e3) {
                    Log.i(LOGTAG, "Exception getting drawable", e3);
                }

            } catch (Exception e4) {
              Log.i(LOGTAG, "Exception getting drawable", e4);
            }

            resourceUrl = null;
        }
        return icon;
    }

    public static Bitmap getLauncherIcon(Context context, Bitmap aSource, int size) {
        final int kOffset = 6;
        final int kRadius = 5;
        int insetSize = aSource != null ? size * 2 / 3 : size;

        Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);


        // draw a base color
        Paint paint = new Paint();
        if (aSource == null) {
            // If we aren't drawing a favicon, just use an orange color.
            paint.setColor(Color.HSVToColor(DEFAULT_LAUNCHER_ICON_HSV));
            canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint);
        } else if (aSource.getWidth() >= insetSize || aSource.getHeight() >= insetSize) {
            // Otherwise, if the icon is large enough, just draw it.
            Rect iconBounds = new Rect(0, 0, size, size);
            canvas.drawBitmap(aSource, null, iconBounds, null);
            return bitmap;
        } else {
            // otherwise use the dominant color from the icon + a layer of transparent white to lighten it somewhat
            int color = BitmapUtils.getDominantColor(aSource);
            paint.setColor(color);
            canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint);
            paint.setColor(Color.argb(100, 255, 255, 255));
            canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint);
        }

        // draw the overlay
        Bitmap overlay = BitmapUtils.decodeResource(context, R.drawable.home_bg);
        canvas.drawBitmap(overlay, null, new Rect(0, 0, size, size), null);

        // draw the favicon
        if (aSource == null)
            aSource = BitmapUtils.decodeResource(context, R.drawable.home_star);

        // by default, we scale the icon to this size
        int sWidth = insetSize / 2;
        int sHeight = sWidth;

        int halfSize = size / 2;
        canvas.drawBitmap(aSource,
                null,
                new Rect(halfSize - sWidth,
                        halfSize - sHeight,
                        halfSize + sWidth,
                        halfSize + sHeight),
                null);

        return bitmap;
    }
}
