package com.getbase.floatingactionbutton;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.graphics.Shader;
import android.graphics.Shader.TileMode;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.ShapeDrawable.ShaderFactory;
import android.graphics.drawable.StateListDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.support.annotation.ColorRes;
import android.support.annotation.DimenRes;
import android.support.annotation.DrawableRes;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.widget.ImageButton;
import android.widget.TextView;

public class FloatingActionButton extends ImageButton {

  public static final int SIZE_NORMAL = 0;
  public static final int SIZE_MINI = 1;

  @Retention(RetentionPolicy.SOURCE)
  @IntDef({ SIZE_NORMAL, SIZE_MINI })
  public @interface FAB_SIZE {
  }

  int mColorNormal;
  int mColorPressed;
  int mColorDisabled;
  String mTitle;
  @DrawableRes
  private int mIcon;
  private Drawable mIconDrawable;
  private int mSize;

  private float mCircleSize;
  private float mShadowRadius;
  private float mShadowOffset;
  private int mDrawableSize;
  boolean mStrokeVisible;

  public FloatingActionButton(Context context) {
    this(context, null);
  }

  public FloatingActionButton(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context, attrs);
  }

  public FloatingActionButton(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    init(context, attrs);
  }

  void init(Context context, AttributeSet attributeSet) {
    TypedArray attr = context.obtainStyledAttributes(attributeSet, R.styleable.FloatingActionButton, 0, 0);
    mColorNormal = attr.getColor(R.styleable.FloatingActionButton_fab_colorNormal, getColor(android.R.color.background_dark));
    mColorPressed = attr.getColor(R.styleable.FloatingActionButton_fab_colorPressed, getColor(android.R.color.background_dark));
    mColorDisabled = attr.getColor(R.styleable.FloatingActionButton_fab_colorDisabled, getColor(android.R.color.darker_gray));
    mSize = attr.getInt(R.styleable.FloatingActionButton_fab_size, SIZE_NORMAL);
    mIcon = attr.getResourceId(R.styleable.FloatingActionButton_fab_icon, 0);
    mTitle = attr.getString(R.styleable.FloatingActionButton_fab_title);
    mStrokeVisible = attr.getBoolean(R.styleable.FloatingActionButton_fab_stroke_visible, true);
    attr.recycle();

    updateCircleSize();
    mShadowRadius = getDimension(R.dimen.fab_shadow_radius);
    mShadowOffset = getDimension(R.dimen.fab_shadow_offset);
    updateDrawableSize();

    updateBackground();
  }

  private void updateDrawableSize() {
    mDrawableSize = (int) (mCircleSize + 2 * mShadowRadius);
  }

  private void updateCircleSize() {
    mCircleSize = getDimension(mSize == SIZE_NORMAL ? R.dimen.fab_size_normal : R.dimen.fab_size_mini);
  }

  public void setSize(@FAB_SIZE int size) {
    if (size != SIZE_MINI && size != SIZE_NORMAL) {
      throw new IllegalArgumentException("Use @FAB_SIZE constants only!");
    }

    if (mSize != size) {
      mSize = size;
      updateCircleSize();
      updateDrawableSize();
      updateBackground();
    }
  }

  @FAB_SIZE
  public int getSize() {
    return mSize;
  }

  public void setIcon(@DrawableRes int icon) {
    if (mIcon != icon) {
      mIcon = icon;
      mIconDrawable = null;
      updateBackground();
    }
  }

  public void setIconDrawable(@NonNull Drawable iconDrawable) {
    if (mIconDrawable != iconDrawable) {
      mIcon = 0;
      mIconDrawable = iconDrawable;
      updateBackground();
    }
  }

  /**
   * @return the current Color for normal state.
   */
  public int getColorNormal() {
    return mColorNormal;
  }

  public void setColorNormalResId(@ColorRes int colorNormal) {
    setColorNormal(getColor(colorNormal));
  }

  public void setColorNormal(int color) {
    if (mColorNormal != color) {
      mColorNormal = color;
      updateBackground();
    }
  }

  /**
   * @return the current color for pressed state.
   */
  public int getColorPressed() {
    return mColorPressed;
  }

  public void setColorPressedResId(@ColorRes int colorPressed) {
    setColorPressed(getColor(colorPressed));
  }

  public void setColorPressed(int color) {
    if (mColorPressed != color) {
      mColorPressed = color;
      updateBackground();
    }
  }

  /**
  * @return the current color for disabled state.
  */
  public int getColorDisabled() {
    return mColorDisabled;
  }

  public void setColorDisabledResId(@ColorRes int colorDisabled) {
    setColorDisabled(getColor(colorDisabled));
  }

  public void setColorDisabled(int color) {
    if (mColorDisabled != color) {
        mColorDisabled = color;
        updateBackground();
    }
  }

  public void setStrokeVisible(boolean visible) {
    if (mStrokeVisible != visible) {
      mStrokeVisible = visible;
      updateBackground();
    }
  }

  public boolean isStrokeVisible() {
    return mStrokeVisible;
  }

  int getColor(@ColorRes int id) {
    return getResources().getColor(id);
  }

  float getDimension(@DimenRes int id) {
    return getResources().getDimension(id);
  }

  public void setTitle(String title) {
    mTitle = title;
    TextView label = getLabelView();
    if (label != null) {
      label.setText(title);
    }
  }

  TextView getLabelView() {
    return (TextView) getTag(R.id.fab_label);
  }

  public String getTitle() {
    return mTitle;
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    setMeasuredDimension(mDrawableSize, mDrawableSize);
  }

  void updateBackground() {
    final float strokeWidth = getDimension(R.dimen.fab_stroke_width);
    final float halfStrokeWidth = strokeWidth / 2f;

    LayerDrawable layerDrawable = new LayerDrawable(
        new Drawable[] {
            getResources().getDrawable(mSize == SIZE_NORMAL ? R.drawable.fab_bg_normal : R.drawable.fab_bg_mini),
            createFillDrawable(strokeWidth),
            createOuterStrokeDrawable(strokeWidth),
            getIconDrawable()
        });

    int iconOffset = (int) (mCircleSize - getDimension(R.dimen.fab_icon_size)) / 2;

    int circleInsetHorizontal = (int) (mShadowRadius);
    int circleInsetTop = (int) (mShadowRadius - mShadowOffset);
    int circleInsetBottom = (int) (mShadowRadius + mShadowOffset);

    layerDrawable.setLayerInset(1,
        circleInsetHorizontal,
        circleInsetTop,
        circleInsetHorizontal,
        circleInsetBottom);

    layerDrawable.setLayerInset(2,
        (int) (circleInsetHorizontal - halfStrokeWidth),
        (int) (circleInsetTop - halfStrokeWidth),
        (int) (circleInsetHorizontal - halfStrokeWidth),
        (int) (circleInsetBottom - halfStrokeWidth));

    layerDrawable.setLayerInset(3,
        circleInsetHorizontal + iconOffset,
        circleInsetTop + iconOffset,
        circleInsetHorizontal + iconOffset,
        circleInsetBottom + iconOffset);

    setBackgroundCompat(layerDrawable);
  }

  Drawable getIconDrawable() {
    if (mIconDrawable != null) {
      return mIconDrawable;
    } else if (mIcon != 0) {
      return getResources().getDrawable(mIcon);
    } else {
      return new ColorDrawable(Color.TRANSPARENT);
    }
  }

  private StateListDrawable createFillDrawable(float strokeWidth) {
    StateListDrawable drawable = new StateListDrawable();
    drawable.addState(new int[] { -android.R.attr.state_enabled }, createCircleDrawable(mColorDisabled, strokeWidth));
    drawable.addState(new int[] { android.R.attr.state_pressed }, createCircleDrawable(mColorPressed, strokeWidth));
    drawable.addState(new int[] { }, createCircleDrawable(mColorNormal, strokeWidth));
    return drawable;
  }

  private Drawable createCircleDrawable(int color, float strokeWidth) {
    int alpha = Color.alpha(color);
    int opaqueColor = opaque(color);

    ShapeDrawable fillDrawable = new ShapeDrawable(new OvalShape());

    final Paint paint = fillDrawable.getPaint();
    paint.setAntiAlias(true);
    paint.setColor(opaqueColor);

    Drawable[] layers = {
        fillDrawable,
        createInnerStrokesDrawable(opaqueColor, strokeWidth)
    };

    LayerDrawable drawable = alpha == 255 || !mStrokeVisible
        ? new LayerDrawable(layers)
        : new TranslucentLayerDrawable(alpha, layers);

    int halfStrokeWidth = (int) (strokeWidth / 2f);
    drawable.setLayerInset(1, halfStrokeWidth, halfStrokeWidth, halfStrokeWidth, halfStrokeWidth);

    return drawable;
  }

  private static class TranslucentLayerDrawable extends LayerDrawable {
    private final int mAlpha;

    public TranslucentLayerDrawable(int alpha, Drawable... layers) {
      super(layers);
      mAlpha = alpha;
    }

    @Override
    public void draw(Canvas canvas) {
      Rect bounds = getBounds();
      canvas.saveLayerAlpha(bounds.left, bounds.top, bounds.right, bounds.bottom, mAlpha, Canvas.ALL_SAVE_FLAG);
      super.draw(canvas);
      canvas.restore();
    }
  }

  private Drawable createOuterStrokeDrawable(float strokeWidth) {
    ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape());

    final Paint paint = shapeDrawable.getPaint();
    paint.setAntiAlias(true);
    paint.setStrokeWidth(strokeWidth);
    paint.setStyle(Style.STROKE);
    paint.setColor(Color.BLACK);
    paint.setAlpha(opacityToAlpha(0.02f));

    return shapeDrawable;
  }

  private int opacityToAlpha(float opacity) {
    return (int) (255f * opacity);
  }

  private int darkenColor(int argb) {
    return adjustColorBrightness(argb, 0.9f);
  }

  private int lightenColor(int argb) {
    return adjustColorBrightness(argb, 1.1f);
  }

  private int adjustColorBrightness(int argb, float factor) {
    float[] hsv = new float[3];
    Color.colorToHSV(argb, hsv);

    hsv[2] = Math.min(hsv[2] * factor, 1f);

    return Color.HSVToColor(Color.alpha(argb), hsv);
  }

  private int halfTransparent(int argb) {
    return Color.argb(
        Color.alpha(argb) / 2,
        Color.red(argb),
        Color.green(argb),
        Color.blue(argb)
    );
  }

  private int opaque(int argb) {
    return Color.rgb(
        Color.red(argb),
        Color.green(argb),
        Color.blue(argb)
    );
  }

  private Drawable createInnerStrokesDrawable(final int color, float strokeWidth) {
    if (!mStrokeVisible) {
      return new ColorDrawable(Color.TRANSPARENT);
    }

    ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape());

    final int bottomStrokeColor = darkenColor(color);
    final int bottomStrokeColorHalfTransparent = halfTransparent(bottomStrokeColor);
    final int topStrokeColor = lightenColor(color);
    final int topStrokeColorHalfTransparent = halfTransparent(topStrokeColor);

    final Paint paint = shapeDrawable.getPaint();
    paint.setAntiAlias(true);
    paint.setStrokeWidth(strokeWidth);
    paint.setStyle(Style.STROKE);
    shapeDrawable.setShaderFactory(new ShaderFactory() {
      @Override
      public Shader resize(int width, int height) {
        return new LinearGradient(width / 2, 0, width / 2, height,
            new int[] { topStrokeColor, topStrokeColorHalfTransparent, color, bottomStrokeColorHalfTransparent, bottomStrokeColor },
            new float[] { 0f, 0.2f, 0.5f, 0.8f, 1f },
            TileMode.CLAMP
        );
      }
    });

    return shapeDrawable;
  }

  @SuppressWarnings("deprecation")
  @SuppressLint("NewApi")
  private void setBackgroundCompat(Drawable drawable) {
    if (Build.VERSION.SDK_INT >= VERSION_CODES.BASE) {
      setBackgroundDrawable(drawable);
    } else {
      setBackgroundDrawable(drawable);
    }
  }

  @Override
  public void setVisibility(int visibility) {
    TextView label = getLabelView();
    if (label != null) {
      label.setVisibility(visibility);
    }

    super.setVisibility(visibility);
  }
}