package com.tylersuehr.esr;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.DrawableRes;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.GravityCompat;
import android.support.v4.view.ViewCompat;
import android.view.Gravity;

/**
 * Copyright © 2017 Tyler Suehr
 *
 * This implementation of {@link EmptyStateRecyclerView.StateDisplay} allows you to display
 * an image and manipulate it.
 *
 * With this you can do the following:
 * (1) Set any Drawable or Bitmap as the image
 * (2) Stretch or crop the image using scale type
 * (3) Align the image using gravity
 * (4) Set margins to adjust image alignment
 *
 * @see Builder to easily instantiate this
 *
 * @author Tyler Suehr
 * @version 1.0
 */
public class ImageStateDisplay extends AbstractStateDisplay {
    /* Constants for image scale type */
    public static final byte NONE           = 0; // No scaling will be applied
    public static final byte FIT_XY         = 1; // Stretch to fit screen dimensions
    public static final byte FIT_WIDTH      = 2; // Stretch to fit screen width
    public static final byte FIT_HEIGHT     = 3; // Stretch to fit screen height
    public static final byte CROP_TO_WIDTH  = 4; // Cropped to fit screen width
    public static final byte CROP_TO_HEIGHT = 5; // Cropped to fit screen height
    public static final byte CROP_XY        = 6; // Cropped to fit larges screen dimension

    private boolean imageConfigured = false;

    /* Stores the scale type to apply to the image */
    private byte scaleType = NONE;
    /* Stores the gravity for the image */
    private int imageGravity;
    /* Stores a reference to the image */
    private Bitmap image;


    @Override
    public void onDrawState(EmptyStateRecyclerView rv, Canvas canvas) {
        final int width = rv.getMeasuredWidth();
        final int height = rv.getMeasuredHeight();
        configureImage(width, height);

        final int horizontalGravity = Gravity.getAbsoluteGravity(imageGravity,
                ViewCompat.getLayoutDirection(rv))&Gravity.HORIZONTAL_GRAVITY_MASK;
        final int verticalGravity = imageGravity&Gravity.VERTICAL_GRAVITY_MASK;

        // Account for horizontal gravity
        float dx;
        switch (horizontalGravity) {
            case Gravity.CENTER_HORIZONTAL:
                dx = (width >> 1) - (image.getWidth() >> 1);
                break;
            case GravityCompat.END:
                dx = width - image.getWidth();
                break;
            default:
            case GravityCompat.START:
                dx = 0;
                break;
        }

        // Account for vertical gravity
        float dy;
        switch (verticalGravity) {
            case Gravity.CENTER_VERTICAL:
                dy = (height >> 1) - (image.getHeight() >> 1);
                break;
            case Gravity.BOTTOM:
                dy = height - image.getHeight();
                break;
            default:
            case Gravity.TOP:
                dy = 0;
                break;
        }

        // Account for the set margins
        dx -= getPaddingLeft(); // Left margin
        dx += getPaddingRight(); // Right margin
        dy += getPaddingTop(); // Top margin
        dy -= getPaddingBottom(); // Bottom margin

        // Draw bitmap using locations based on gravity
        canvas.drawBitmap(image, dx, dy, null);
    }

    public void setScaleType(byte scaleType) {
        this.scaleType = scaleType;
        invalidateImage();
    }

    public void setImageGravity(int gravity) {
        this.imageGravity = gravity;
        // No need for invalidation
    }

    public void setImage(Bitmap bitmap) {
        this.image = bitmap;
        invalidateImage();
    }

    public void setImage(Drawable drawable) {
        this.image = drawableToBitmap(drawable);
        invalidateImage();
    }

    public void setImage(Context c, @DrawableRes int res) {
        setImage(ContextCompat.getDrawable(c, res));
    }

    public void resizeImage(int width, int height) {
        if (image == null) {
            throw new NullPointerException("Please set an image before calling resizeImage()!");
        }
        this.image = Bitmap.createScaledBitmap(image, width, height, false);
        invalidateImage();
    }

    protected void stretchImage(final int screenWidth, final int screenHeight) {
        switch (scaleType) {
            case FIT_XY:
                this.image = Bitmap.createScaledBitmap(image, screenWidth, screenHeight, true);
                break;
            case FIT_WIDTH:
                this.image = Bitmap.createScaledBitmap(image, screenWidth, image.getHeight(), true);
                break;
            case FIT_HEIGHT:
                this.image = Bitmap.createScaledBitmap(image, image.getWidth(), screenHeight, true);
                break;
        }
    }

    protected void cropImage(final int screenWidth, final int screenHeight) {
        final int sourceWidth = image.getWidth();
        final int sourceHeight = image.getHeight();

        // Compute the scaling factors to fit the new height and width, respectively.
        final float xScale = (float)screenWidth / sourceWidth;
        final float yScale = (float)screenHeight / sourceHeight;
        final float scale;
        switch (scaleType) {
            case CROP_TO_WIDTH: // Final scaling will be the width scale
                scale = xScale;
                break;
            case CROP_TO_HEIGHT: // Final scaling will be the height scale
                scale = yScale;
                break;
            default: // Final scaling will be the bigger of the width and height
                scale = Math.max(xScale, yScale);
                break;
        }

        // Now get the size of the source bitmap when scaled
        float scaledWidth = scale * sourceWidth;
        float scaledHeight = scale * sourceHeight;

        // Let's find out the upper left coordinates if the scaled bitmap
        // should be centered in the new size give by the parameters
        float left = (screenWidth - scaledWidth) / 2;
        float top = (screenHeight - scaledHeight) / 2;

        // The target rectangle for the new, scaled version of the source
        // bitmap will now be
        RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight);

        // Finally, we create a new bitmap of the specified size and draw our new,
        // scaled bitmap onto it.
        Bitmap dest = Bitmap.createBitmap(screenWidth, screenHeight, image.getConfig());
        Canvas canvas = new Canvas(dest);
        canvas.drawBitmap(image, null, targetRect, null);
        this.image = dest;
    }

    protected static Bitmap drawableToBitmap(Drawable dr) {
        if (dr instanceof BitmapDrawable) {
            BitmapDrawable bpDr = (BitmapDrawable)dr;
            if (bpDr.getBitmap() != null) {
                return bpDr.getBitmap();
            }
        }

        final Bitmap bitmap;
        if (dr.getIntrinsicWidth() <= 0 || dr.getIntrinsicHeight() <= 0) {
            bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
        } else {
            bitmap = Bitmap.createBitmap(dr.getIntrinsicWidth(), dr.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
        }

        Canvas canvas = new Canvas(bitmap);
        dr.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        dr.draw(canvas);
        return bitmap;
    }

    private void invalidateImage() {
        this.imageConfigured = false;
    }

    private void configureImage(final int width, final int height) {
        if (!imageConfigured) {
            switch (scaleType) {
                case FIT_XY:
                case FIT_WIDTH:
                case FIT_HEIGHT:
                    stretchImage(width, height);
                    break;
                case CROP_XY:
                case CROP_TO_WIDTH:
                case CROP_TO_HEIGHT:
                    cropImage(width, height);
                    break;
            }
            imageConfigured = true;
        }
    }


    /**
     * Internal class to help instantiate {@link ImageStateDisplay}.
     */
    public static final class Builder {
        private final Context c;
        private final int[] padding = { 0, 0, 0, 0 };
        private byte scaleType;
        private int gravity;
        private Bitmap image;


        public Builder(Context c) {
            this.c = c;
        }

        public Builder setImage(@DrawableRes int res) {
            return setImage(ContextCompat.getDrawable(c, res));
        }

        public Builder setImage(Drawable dr) {
            this.image = drawableToBitmap(dr);
            return this;
        }

        public Builder setImage(Bitmap bp) {
            this.image = bp;
            return this;
        }

        public Builder resizeImage(int widthDp, int heightDp) {
            if (image == null) {
                throw new NullPointerException("Please set image before calling resizeImage()!");
            }

            final float density = c.getResources().getDisplayMetrics().density;
            final int desiredWidth = (int)(widthDp * density);
            final int desiredHeight = (int)(heightDp * density);

            this.image = Bitmap.createScaledBitmap(image, desiredWidth, desiredHeight, false);
            return this;
        }

        public Builder setImageGravity(int gravity) {
            this.gravity = gravity;
            return this;
        }

        public Builder setScaleType(byte scaleType) {
            this.scaleType = scaleType;
            return this;
        }

        public Builder setPadding(int leftDp, int topDp, int rightDp, int bottomDp) {
            final float density = c.getResources().getDisplayMetrics().density;
            this.padding[0] = (int)(leftDp * density);
            this.padding[1] = (int)(topDp * density);
            this.padding[2] = (int)(rightDp * density);
            this.padding[3] = (int)(bottomDp * density);
            return this;
        }

        public ImageStateDisplay build() {
            if (image == null) {
                throw new NullPointerException("Image cannot be null!");
            }

            ImageStateDisplay state = new ImageStateDisplay();
            state.setPadding(padding[0], padding[1], padding[2], padding[3]);
            state.scaleType = scaleType;
            state.imageGravity = gravity;
            state.image = image;
            return state;
        }
    }
}