package org.odk.collect.android.views.drawing;

import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v7.widget.CardView;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.FrameLayout;
import android.widget.ImageView;

import org.odk.collect.android.R;
import org.odk.collect.android.utilities.DrawPreferences;
import org.odk.collect.android.utilities.FileUtils;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.ArrayList;
import java.util.List;

/**
 * A View that can be drawn on by the user.
 */
public class DrawView extends FrameLayout implements View.OnTouchListener {

  private static final String TAG = "DrawView";
  private static final int ZOOM_VIEW_MARGIN = 12;
  private static final int QUARTER = 4;

  // LISTENER
  private List<OnDrawViewListener> listeners;
  private ScaleGestureDetector mScaleGestureDetector;
  private GestureDetector mGestureDetector;

  // VARS
  private boolean isForCamera = false;

  private int mDrawColor;
  private int mDrawWidth;
  private int mDrawAlpha;
  private boolean mAntiAlias;
  private boolean mDither;
  private SerializablePaint.Style mPaintStyle;
  private SerializablePaint.Cap mLineCap;
  private Paint bitmapPaint = new Paint(Paint.DITHER_FLAG);
  private Typeface mFontFamily;
  private float mFontSize;
  private int mBackgroundColor = -1;
  private Bitmap mBackgroundImageBitmap;
  private Rect mCanvasClipBounds;

  private Bitmap initialBitmap;
  private Bitmap mContentBitmap;
  private Canvas mContentCanvas;

  private boolean mZoomEnabled = false;
  private float mZoomFactor = 1.0f;
  private float mZoomCenterX = -1.0f;
  private float mZoomCenterY = -1.0f;
  private float mMaxZoomFactor = 8f;
  private float mZoomRegionScale = 4f;
  private float mZoomRegionScaleMin = 2f;
  private float mZoomRegionScaleMax = 5f;
  private boolean mFromZoomRegion = false;

  private int mLastTouchEvent = -1;

  private DrawingMode mDrawingMode;
  private DrawingTool mDrawingTool;
  private DrawingOrientation mInitialDrawingOrientation;

  private List<DrawMove> mDrawMoveHistory;
  private int mDrawMoveHistoryIndex = -1;
  private int mDrawMoveBackgroundIndex = -1;

  private RectF mAuxRect;
  private PorterDuffXfermode mEraserXefferMode;
  private SerializablePaint mBackgroundPaint;

  private Rect mInvalidateRect;

  // VIEWS
  private CardView mZoomRegionCardView;
  private ZoomRegionView mZoomRegionView;
  private Bitmap backgroundBitmap;
  private boolean touched = false;

  /**
   * Default constructor
   *
   * @param context
   */
  public DrawView(Context context) {
    this(context, null);
  }

  /**
   * Default constructor
   *
   * @param context
   * @param attrs
   */
  public DrawView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }

  /**
   * Default constructor
   *
   * @param context
   * @param attrs
   * @param defStyleAttr
   */
  public DrawView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    initVars();
    initAttributes(context, attrs);
  }

  /**
   * Default constructor
   *
   * @param context
   * @param attrs
   * @param defStyleAttr
   * @param defStyleRes
   */
  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  public DrawView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    initVars();
    initAttributes(context, attrs);
  }

  /**
   * @param bitmap
   *     the initial drawing, such as edits made in previous sessions. Is drawn on top of the background but under the
   *     additions drawn during this session.
   */
  public void setInitialBitmap(final Bitmap bitmap) {
    initialBitmap = Bitmap.createScaledBitmap(bitmap, getWidth(), getHeight(), false);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected void onDraw(Canvas canvas) {

    mContentBitmap.eraseColor(Color.TRANSPARENT);

    if (isZoomEnabled()) {
      canvas.save();
      canvas.scale(mZoomFactor, mZoomFactor, mZoomCenterX, mZoomCenterY);
    }

    // Draw canvas background
    canvas.drawRect(0, 0, mContentBitmap.getWidth(), mContentBitmap.getHeight(), mBackgroundPaint);

    if (backgroundBitmap != null) {
      canvas.drawBitmap(backgroundBitmap, 0, 0, null);
    }

    if (initialBitmap != null) {
      mContentCanvas.drawBitmap(initialBitmap, 0, 0, bitmapPaint);
    }

    for (int i = 0; i < mDrawMoveHistoryIndex + 1; i++) {
      DrawMove drawMove = mDrawMoveHistory.get(i);
      if (drawMove.getDrawingMode() != null) {
        switch (drawMove.getDrawingMode()) {
          case DRAW:
            switch (drawMove.getDrawingTool()) {
              case PEN:
                if (drawMove.getDrawingPath() != null) {
                  mContentCanvas.drawPath(drawMove.getDrawingPath(), drawMove.getPaint());
                }
                break;
              case LINE:
                mContentCanvas.drawLine(drawMove.getStartX(), drawMove.getStartY(),
                    drawMove.getEndX(), drawMove.getEndY(), drawMove.getPaint());
                break;
              case ARROW:
                mContentCanvas.drawLine(drawMove.getStartX(), drawMove.getStartY(),
                    drawMove.getEndX(), drawMove.getEndY(), drawMove.getPaint());
                float angle = (float) Math.toDegrees(Math.atan2(drawMove.getEndY() - drawMove.getStartY(),
                    drawMove.getEndX() - drawMove.getStartX())) - 90;
                angle = angle < 0 ? angle + 360 : angle;
                float middleWidth = 8f + drawMove.getPaint().getStrokeWidth();
                float arrowHeadLarge = 30f + drawMove.getPaint().getStrokeWidth();

                mContentCanvas.save();
                mContentCanvas.translate(drawMove.getEndX(), drawMove.getEndY());
                mContentCanvas.rotate(angle);
                mContentCanvas.drawLine(0f, 0f, middleWidth, 0f, drawMove.getPaint());
                mContentCanvas.drawLine(middleWidth, 0f, 0f, arrowHeadLarge, drawMove.getPaint());
                mContentCanvas.drawLine(0f, arrowHeadLarge, -middleWidth, 0f, drawMove.getPaint());
                mContentCanvas.drawLine(-middleWidth, 0f, 0f, 0f, drawMove.getPaint());
                mContentCanvas.restore();

                break;
              case RECTANGLE:
                mContentCanvas.drawRect(drawMove.getStartX(), drawMove.getStartY(),
                    drawMove.getEndX(), drawMove.getEndY(), drawMove.getPaint());
                break;
              case CIRCLE:
                if (drawMove.getEndX() > drawMove.getStartX()) {
                  mContentCanvas.drawCircle(drawMove.getStartX(), drawMove.getStartY(),
                      drawMove.getEndX() - drawMove.getStartX(), drawMove.getPaint());
                } else {
                  mContentCanvas.drawCircle(drawMove.getStartX(), drawMove.getStartY(),
                      drawMove.getStartX() - drawMove.getEndX(), drawMove.getPaint());
                }
                break;
              case ELLIPSE:
                mAuxRect.set(drawMove.getEndX() - Math.abs(drawMove.getEndX() - drawMove.getStartX()),
                    drawMove.getEndY() - Math.abs(drawMove.getEndY() - drawMove.getStartY()),
                    drawMove.getEndX() + Math.abs(drawMove.getEndX() - drawMove.getStartX()),
                    drawMove.getEndY() + Math.abs(drawMove.getEndY() - drawMove.getStartY()));
                mContentCanvas.drawOval(mAuxRect, drawMove.getPaint());
                break;
            }
            break;
          case TEXT:
            if (drawMove.getText() != null && !drawMove.getText().equals("")) {
              mContentCanvas.drawText(drawMove.getText(), drawMove.getEndX(), drawMove.getEndY(), drawMove.getPaint());
            }
            break;
          case ERASER:
            if (drawMove.getDrawingPath() != null) {
              drawMove.getPaint().setXfermode(mEraserXefferMode);
              mContentCanvas.drawPath(drawMove.getDrawingPath(), drawMove.getPaint());
              drawMove.getPaint().setXfermode(null);
            }
            break;
        }
      }

      if (i == mDrawMoveHistory.size() - 1 && listeners != null && !listeners.isEmpty()) {
        for (final OnDrawViewListener onDrawViewListener : listeners) {
          onDrawViewListener.onAllMovesPainted();
        }
      }
    }

    canvas.getClipBounds(mCanvasClipBounds);
    canvas.drawBitmap(mContentBitmap, 0, 0, null);

    if (isZoomEnabled()) {
      canvas.restore();

      if (mZoomRegionView != null && !mFromZoomRegion) {
        Bitmap bmOverlay;
        if (backgroundBitmap != null) {
          bmOverlay = Bitmap.createBitmap(backgroundBitmap.getWidth(), backgroundBitmap.getHeight(),
              backgroundBitmap.getConfig());
        } else {
          bmOverlay = Bitmap.createBitmap(mContentBitmap.getWidth(), mContentBitmap.getHeight(), mContentBitmap
              .getConfig());
        }
        Canvas tempCanvas = new Canvas(bmOverlay);
        if (backgroundBitmap != null) {
          tempCanvas.drawBitmap(backgroundBitmap, new Matrix(), null);
        }
        tempCanvas.drawBitmap(mContentBitmap, 0, 0, null);
        mZoomRegionView.drawZoomRegion(bmOverlay, mCanvasClipBounds, QUARTER);
      }
    }

    super.onDraw(canvas);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean onTouch(View view, MotionEvent motionEvent) {
    touched = true;
    if (mZoomEnabled) {
      mScaleGestureDetector.onTouchEvent(motionEvent);
      mGestureDetector.onTouchEvent(motionEvent);
    }
    float touchX = motionEvent.getX() / mZoomFactor + mCanvasClipBounds.left;
    float touchY = motionEvent.getY() / mZoomFactor + mCanvasClipBounds.top;

    int lastMoveIndex = 0;

    if (motionEvent.getPointerCount() == 1) {
      switch (motionEvent.getActionMasked()) {
        case MotionEvent.ACTION_DOWN:
          mLastTouchEvent = MotionEvent.ACTION_DOWN;

          if (listeners != null && !listeners.isEmpty()) {
            for (final OnDrawViewListener onDrawViewListener : listeners) {
              onDrawViewListener.onStartDrawing();
            }
          }

          if (mDrawMoveHistoryIndex >= -1 &&
              mDrawMoveHistoryIndex < mDrawMoveHistory.size() - 1) {
            mDrawMoveHistory = mDrawMoveHistory.subList(0, mDrawMoveHistoryIndex + 1);
          }

          mDrawMoveHistory.add(DrawMove.newInstance()
              .setPaint(getNewPaintParams())
              .setStartX(touchX).setStartY(touchY)
              .setEndX(touchX).setEndY(touchY)
              .setDrawingMode(mDrawingMode).setDrawingTool(mDrawingTool));
          lastMoveIndex = mDrawMoveHistory.size() - 1;

          mDrawMoveHistoryIndex++;

          if (mDrawingTool == DrawingTool.PEN || mDrawingMode == DrawingMode.ERASER) {
            SerializablePath path = new SerializablePath();
            path.moveTo(touchX, touchY);
            path.lineTo(touchX, touchY);

            mDrawMoveHistory.get(lastMoveIndex).setDrawingPathList(path);
          }
          break;
        case MotionEvent.ACTION_MOVE:
          if ((mLastTouchEvent == MotionEvent.ACTION_DOWN ||
              mLastTouchEvent == MotionEvent.ACTION_MOVE)) {
            mLastTouchEvent = MotionEvent.ACTION_MOVE;

            lastMoveIndex = mDrawMoveHistory.size() - 1;

            if (mDrawMoveHistory.size() > 0) {
              mDrawMoveHistory.get(lastMoveIndex).setEndX(touchX).setEndY(touchY);

              if (mDrawingTool == DrawingTool.PEN || mDrawingMode == DrawingMode.ERASER) {
                for (int i = 0; i < motionEvent.getHistorySize(); i++) {
                  float historicalX = motionEvent.getHistoricalX(i) / mZoomFactor + mCanvasClipBounds.left;
                  float historicalY = motionEvent.getHistoricalY(i) / mZoomFactor + mCanvasClipBounds.top;
                  mDrawMoveHistory.get(lastMoveIndex).getDrawingPath().lineTo(historicalX, historicalY);
                }
                mDrawMoveHistory.get(lastMoveIndex).getDrawingPath().lineTo(touchX, touchY);
              }
            }
          }
          break;
        case MotionEvent.ACTION_UP:
          lastMoveIndex = mDrawMoveHistory.size() - 1;

          if (mLastTouchEvent == MotionEvent.ACTION_DOWN) {
            if (mDrawMoveHistory.size() > 0 && mDrawingMode != DrawingMode.TEXT) {
              mDrawMoveHistory.remove(lastMoveIndex);
              mDrawMoveHistoryIndex--;
              lastMoveIndex--;
            }
          } else if (mLastTouchEvent == MotionEvent.ACTION_MOVE) {
            mLastTouchEvent = -1;
            if (mDrawMoveHistory.size() > 0) {
              mDrawMoveHistory.get(lastMoveIndex).setEndX(touchX).setEndY(touchY);

              if (mDrawingTool == DrawingTool.PEN || mDrawingMode == DrawingMode.ERASER) {
                for (int i = 0; i < motionEvent.getHistorySize(); i++) {
                  float historicalX = motionEvent.getHistoricalX(i) / mZoomFactor + mCanvasClipBounds.left;
                  float historicalY = motionEvent.getHistoricalY(i) / mZoomFactor + mCanvasClipBounds.top;
                  mDrawMoveHistory.get(lastMoveIndex).getDrawingPath().lineTo(historicalX, historicalY);
                }
                mDrawMoveHistory.get(lastMoveIndex).getDrawingPath().lineTo(touchX, touchY);
              }
            }
          }

          if (listeners != null && !listeners.isEmpty()) {
            for (final OnDrawViewListener onDrawViewListener : listeners) {
              if (mDrawingMode == DrawingMode.TEXT) {
                onDrawViewListener.onRequestText();
              }
              onDrawViewListener.onEndDrawing();
            }
          }

          break;
        default:
          return false;
      }
    } else {
      mLastTouchEvent = -1;
    }

    if (mDrawMoveHistory.size() > 0) {
      mInvalidateRect = new Rect(
          (int) (touchX - (mDrawMoveHistory.get(lastMoveIndex).getPaint().getStrokeWidth() * 2)),
          (int) (touchY - (mDrawMoveHistory.get(lastMoveIndex).getPaint().getStrokeWidth() * 2)),
          (int) (touchX + (mDrawMoveHistory.get(lastMoveIndex).getPaint().getStrokeWidth() * 2)),
          (int) (touchY + (mDrawMoveHistory.get(lastMoveIndex).getPaint().getStrokeWidth() * 2)));
    }

    this.invalidate(mInvalidateRect.left, mInvalidateRect.top, mInvalidateRect.right, mInvalidateRect.bottom);
    return true;
  }

  @Override
  protected Parcelable onSaveInstanceState() {
    Bundle bundle = new Bundle();
    bundle.putParcelable("superState", super.onSaveInstanceState());
    bundle.putInt("drawMoveHistorySize", mDrawMoveHistory.size());
    for (int i = 0; i < mDrawMoveHistory.size(); i++) {
      bundle.putSerializable("mDrawMoveHistory" + i, mDrawMoveHistory.get(i));
    }
    bundle.putInt("mDrawMoveHistoryIndex", mDrawMoveHistoryIndex);
    bundle.putInt("mDrawMoveBackgroundIndex", mDrawMoveBackgroundIndex);
    bundle.putSerializable("mDrawingMode", mDrawingMode);
    bundle.putSerializable("mDrawingTool", mDrawingTool);
    bundle.putSerializable("mInitialDrawingOrientation", mInitialDrawingOrientation);

    bundle.putInt("mDrawColor", mDrawColor);
    bundle.putInt("mDrawWidth", mDrawWidth);
    bundle.putInt("mDrawAlpha", mDrawAlpha);
    bundle.putInt("mBackgroundColor", mBackgroundColor);
    bundle.putBoolean("mAntiAlias", mAntiAlias);
    bundle.putBoolean("mDither", mDither);
    bundle.putFloat("mFontSize", mFontSize);
    bundle.putSerializable("mPaintStyle", mPaintStyle);
    bundle.putSerializable("mLineCap", mLineCap);
    bundle.putInt("mFontFamily",
        mFontFamily == Typeface.DEFAULT ? 0 :
            mFontFamily == Typeface.MONOSPACE ? 1 :
                mFontFamily == Typeface.SANS_SERIF ? 2 :
                    mFontFamily == Typeface.SERIF ? 3 : 0);
    return bundle;
  }

  @Override
  protected void onRestoreInstanceState(Parcelable state) {
    if (state instanceof Bundle) {
      Bundle bundle = (Bundle) state;
      for (int i = 0; i < bundle.getInt("drawMoveHistorySize"); i++) {
        mDrawMoveHistory.add((DrawMove) bundle.getSerializable("mDrawMoveHistory" + i));
      }
      mDrawMoveHistoryIndex = bundle.getInt("mDrawMoveHistoryIndex");
      mDrawMoveBackgroundIndex = bundle.getInt("mDrawMoveBackgroundIndex");
      mDrawingMode = (DrawingMode) bundle.getSerializable("mDrawingMode");
      mDrawingTool = (DrawingTool) bundle.getSerializable("mDrawingTool");
      mInitialDrawingOrientation = (DrawingOrientation) bundle.getSerializable("mInitialDrawingOrientation");

      mDrawColor = bundle.getInt("mDrawColor");
      mDrawWidth = bundle.getInt("mDrawWidth");
      mDrawAlpha = bundle.getInt("mDrawAlpha");
      mBackgroundColor = bundle.getInt("mBackgroundColor");
      mAntiAlias = bundle.getBoolean("mAntiAlias");
      mDither = bundle.getBoolean("mDither");
      mFontSize = bundle.getFloat("mFontSize");
      mPaintStyle = (SerializablePaint.Style) bundle.getSerializable("mPaintStyle");
      mLineCap = (SerializablePaint.Cap) bundle.getSerializable("mLineCap");
      mFontFamily =
          bundle.getInt("mFontFamily") == 0 ? Typeface.DEFAULT :
              bundle.getInt("mFontFamily") == 1 ? Typeface.MONOSPACE :
                  bundle.getInt("mFontFamily") == 2 ? Typeface.SANS_SERIF :
                      bundle.getInt("mFontFamily") == 3 ? Typeface.SERIF : Typeface.DEFAULT;
      state = bundle.getParcelable("superState");
    }
    super.onRestoreInstanceState(state);
  }

  /**
   * Initialize general vars for the view
   */
  private void initVars() {
    mDrawMoveHistory = new ArrayList<>();
    mScaleGestureDetector = new ScaleGestureDetector(getContext(), new ScaleGestureListener());
    mGestureDetector = new GestureDetector(getContext(), new GestureListener());
    mCanvasClipBounds = new Rect();
    mAuxRect = new RectF();
    mEraserXefferMode = new PorterDuffXfermode(PorterDuff.Mode.CLEAR);

    setOnTouchListener(this);

    getViewTreeObserver().addOnGlobalLayoutListener(
        new ViewTreeObserver.OnGlobalLayoutListener() {

          @SuppressLint("NewApi")
          @SuppressWarnings("deprecation")
          @Override
          public void onGlobalLayout() {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
              getViewTreeObserver()
                  .removeGlobalOnLayoutListener(this);
            } else {
              getViewTreeObserver()
                  .removeOnGlobalLayoutListener(this);
            }
            initZoomRegionView();
          }
        });
  }

  /**
   * Initialise the zoom region view.
   */
  private void initZoomRegionView() {
    if (mZoomRegionView == null) {

      Bitmap init = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
      mContentBitmap = init.copy(Bitmap.Config.ARGB_8888, true);
      init.recycle();
      mContentCanvas = new Canvas(mContentBitmap);

      LayoutParams layoutParams = new LayoutParams(getWidth() / QUARTER, getHeight() / QUARTER, Gravity.TOP | Gravity
          .END);
      layoutParams.setMargins(ZOOM_VIEW_MARGIN, ZOOM_VIEW_MARGIN, ZOOM_VIEW_MARGIN, ZOOM_VIEW_MARGIN);
      mZoomRegionCardView = new CardView(getContext());
      mZoomRegionCardView.setLayoutParams(layoutParams);
      mZoomRegionCardView.setPreventCornerOverlap(true);
      mZoomRegionCardView.setRadius(0f);
      mZoomRegionCardView.setUseCompatPadding(true);
      mZoomRegionCardView.setVisibility(View.INVISIBLE);

      CardView.LayoutParams childLayoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
          ViewGroup.LayoutParams.MATCH_PARENT);
      mZoomRegionView = new ZoomRegionView(getContext());
      mZoomRegionView.setLayoutParams(childLayoutParams);
      mZoomRegionView.setScaleType(ImageView.ScaleType.CENTER_CROP);
      mZoomRegionView.setOnZoomRegionListener(new ZoomRegionView.OnZoomRegionListener() {
        @Override
        public void onZoomRegionMoved(Rect newRect) {
          mFromZoomRegion = true;
          mZoomCenterX = newRect.centerX() * QUARTER;
          mZoomCenterY = newRect.centerY() * QUARTER;

          invalidate();
        }
      });

      mZoomRegionCardView.addView(mZoomRegionView);
      addView(mZoomRegionCardView);
    }
  }

  /**
   * Initialize view attributes
   *
   * @param context
   *     the current Context.
   * @param attrs
   *     the XML defined attributes.
   */
  private void initAttributes(Context context, AttributeSet attrs) {
    TypedArray typedArray = context.getTheme().obtainStyledAttributes(
        attrs, R.styleable.DrawView, 0, 0);
    try {
      final DrawPreferences drawPreferences = new DrawPreferences(context);
      mDrawColor = typedArray.getColor(R.styleable.DrawView_dv_draw_color, drawPreferences.getDrawColor());
      mDrawWidth = typedArray.getInteger(R.styleable.DrawView_dv_draw_width, QUARTER);
      mDrawAlpha = typedArray.getInteger(R.styleable.DrawView_dv_draw_alpha, 255);
      mAntiAlias = typedArray.getBoolean(R.styleable.DrawView_dv_draw_anti_alias, true);
      mDither = typedArray.getBoolean(R.styleable.DrawView_dv_draw_dither, true);
      int paintStyle = typedArray.getInteger(R.styleable.DrawView_dv_draw_style, -1);
      switch (paintStyle) {
        case -1:
          mPaintStyle = drawPreferences.getPaintStyle();
          break;
        case 0:
          mPaintStyle = SerializablePaint.Style.FILL;
          break;
        case 1:
          mPaintStyle = SerializablePaint.Style.FILL_AND_STROKE;
          break;
        case 2:
          mPaintStyle = SerializablePaint.Style.STROKE;
          break;
      }
      int cap = typedArray.getInteger(R.styleable.DrawView_dv_draw_corners, 2);
      if (cap == 0) {
        mLineCap = SerializablePaint.Cap.BUTT;
      } else if (cap == 1) {
        mLineCap = SerializablePaint.Cap.ROUND;
      } else if (cap == 2) {
        mLineCap = SerializablePaint.Cap.SQUARE;
      }
      int typeface = typedArray.getInteger(R.styleable.DrawView_dv_draw_font_family, 0);
      if (typeface == 0) {
        mFontFamily = Typeface.DEFAULT;
      } else if (typeface == 1) {
        mFontFamily = Typeface.MONOSPACE;
      } else if (typeface == 2) {
        mFontFamily = Typeface.SANS_SERIF;
      } else if (typeface == 3) {
        mFontFamily = Typeface.SERIF;
      }
      mFontSize = typedArray.getInteger(R.styleable.DrawView_dv_draw_font_size, drawPreferences.getTextSize());
      isForCamera = typedArray.getBoolean(R.styleable.DrawView_dv_draw_is_camera, false);
      int orientation = typedArray.getInteger(R.styleable.DrawView_dv_draw_orientation,
          getWidth() > getHeight() ? 1 : 0);
      mInitialDrawingOrientation = DrawingOrientation.values()[orientation];
      if (getBackground() != null && !isForCamera) {
        try {
          mBackgroundColor = ((ColorDrawable) getBackground()).getColor();
          setBackgroundColor(Color.TRANSPARENT);
        } catch (Exception e) {
          setBackgroundColor(Color.TRANSPARENT);
          mBackgroundColor = ((ColorDrawable) getBackground()).getColor();
        }
      } else {
        setBackgroundColor(Color.TRANSPARENT);
        mBackgroundColor = ((ColorDrawable) getBackground()).getColor();
      }

      mBackgroundPaint = new SerializablePaint();
      mBackgroundPaint.setStyle(SerializablePaint.Style.FILL);
      mBackgroundPaint.setColor(mBackgroundColor != -1 ? mBackgroundColor : Color.TRANSPARENT);

      final int drawingToolInt = typedArray.getInteger(R.styleable.DrawView_dv_draw_tool, -1);
      if (drawingToolInt == -1) {
        mDrawingTool = drawPreferences.getDrawTool();
      } else {
        mDrawingTool = DrawingTool.values()[drawingToolInt];
      }
      final int drawingModeInt = typedArray.getInteger(R.styleable.DrawView_dv_draw_mode, -1);
      if (drawingModeInt == -1) {
        mDrawingMode = drawPreferences.getDrawMode();
      } else {
        mDrawingMode = DrawingMode.values()[drawingModeInt];
      }
      mZoomEnabled = typedArray.getBoolean(R.styleable.DrawView_dv_draw_enable_zoom, false);
      mZoomRegionScale = typedArray.getFloat(R.styleable.DrawView_dv_draw_zoomregion_scale, mZoomRegionScale);
      mZoomRegionScaleMin = typedArray.getFloat(R.styleable.DrawView_dv_draw_zoomregion_minscale, mZoomRegionScaleMin);
      mZoomRegionScaleMax = typedArray.getFloat(R.styleable.DrawView_dv_draw_zoomregion_maxscale, mZoomRegionScaleMax);
    } finally {
      typedArray.recycle();
    }
  }

  /**
   * @return new paint parameters to initialize the view.
   */
  private SerializablePaint getNewPaintParams() {
    SerializablePaint paint = new SerializablePaint();

    if (mDrawingMode == DrawingMode.ERASER) {
      if (mDrawingTool != DrawingTool.PEN) {
        mDrawingTool = DrawingTool.PEN;
      }
      paint.setColor(mBackgroundColor);
    } else {
      paint.setColor(mDrawColor);
    }

    paint.setStyle(mPaintStyle);
    paint.setDither(mDither);
    paint.setStrokeWidth(mDrawWidth);
    paint.setAlpha(mDrawAlpha);
    paint.setAntiAlias(mAntiAlias);
    paint.setStrokeCap(mLineCap);
    paint.setTypeface(mFontFamily);
    paint.setTextSize(mFontSize);

    return paint;
  }

  /**
   * Restart all the parameters and drawing history.
   *
   * @return if the draw view can be restarted.
   */
  public boolean restartDrawing() {
    touched = true;
    initialBitmap = null;
    if (mDrawMoveHistory != null) {
      mDrawMoveHistory.clear();
      mDrawMoveHistoryIndex = -1;
      mDrawMoveBackgroundIndex = -1;
      invalidate();

      if (listeners != null && !listeners.isEmpty()) {
        for (final OnDrawViewListener onDrawViewListener : listeners) {
          onDrawViewListener.onClearDrawing();
        }
      }

      return true;
    }
    invalidate();
    return false;
  }

  /**
   * Undo the last drawing action.
   *
   * @return if the view can perform the undo action.
   */
  public boolean undo() {
    if (mDrawMoveHistoryIndex > -1 &&
        mDrawMoveHistory.size() > 0) {
      mDrawMoveHistoryIndex--;

      mDrawMoveBackgroundIndex = -1;
      for (int i = 0; i < mDrawMoveHistoryIndex + 1; i++) {
        if (mDrawMoveHistory.get(i).getBackgroundImage() != null) {
          mDrawMoveBackgroundIndex = i;
        }
      }

      invalidate();
      return true;
    }
    invalidate();
    return false;
  }

  /**
   * @return {@code true} if the last action can be undone, {@code false} if it cannot.
   */
  public boolean canUndo() {
    return mDrawMoveHistoryIndex > -1 &&
        mDrawMoveHistory.size() > 0;
  }

  /**
   * Redo the previous action.
   *
   * @return if the view can perform the redo action.
   */
  public boolean redo() {
    if (mDrawMoveHistoryIndex <= mDrawMoveHistory.size() - 1) {
      mDrawMoveHistoryIndex++;

      mDrawMoveBackgroundIndex = -1;
      for (int i = 0; i < mDrawMoveHistoryIndex + 1; i++) {
        if (mDrawMoveHistory.get(i).getBackgroundImage() != null) {
          mDrawMoveBackgroundIndex = i;
        }
      }

      invalidate();
      return true;
    }
    invalidate();
    return false;
  }

  /**
   * @return {@code true} if we can redo an action.
   */
  public boolean canRedo() {
    return mDrawMoveHistoryIndex < mDrawMoveHistory.size() - 1;
  }

  /**
   * Create capture of the drawing view as bitmap or as byte array
   *
   * @param drawingCapture
   *     the desired format of the captured image.
   *
   * @return Object in form of bitmap or byte array.
   */
  public Object[] createCapture(DrawingCapture drawingCapture) {
    Object[] result = null;
    switch (drawingCapture) {
      case BITMAP:
        result = new Object[2];
        result[0] = mContentBitmap;
        result[1] = mBackgroundPaint.getColor() == Color.TRANSPARENT ? "PNG" : "JPG";
        break;
      case BYTES:
        result = new Object[2];
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        mContentBitmap.compress(
            mBackgroundPaint.getColor() == Color.TRANSPARENT ?
                Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG,
            100, stream);
        result[0] = stream.toByteArray();
        result[1] = mBackgroundPaint.getColor() == Color.TRANSPARENT ? "PNG" : "JPG";
        break;
    }
    return result;
  }

  /**
   * Refresh the text of the last movement item
   *
   * @param newText
   *     the text to display.
   */
  public void refreshLastText(String newText) {
    final DrawMove drawMove = mDrawMoveHistory.get(mDrawMoveHistory.size() - 1);
    if (!mDrawMoveHistory.isEmpty() && drawMove
        .getDrawingMode() == DrawingMode.TEXT) {
      drawMove.setText(newText);
      drawMove.getPaint().setTextSize(mFontSize);
      drawMove.getPaint().setTypeface(mFontFamily);
      drawMove.getPaint().setColor(mDrawColor);
      invalidate();
    } else {
      Log.e(TAG, "The last item that you want to refresh text isn't TEXT element.");
    }
  }

  public int getDrawColor() {
    return mDrawColor;
  }

  public boolean isZoomEnabled() {
    return mZoomEnabled;
  }

  public float getMaxZoomFactor() {
    return mMaxZoomFactor;
  }

  /**
   * @param drawColor
   *     the color to be used for the next action.
   *
   * @return this instance of the view.
   */
  public DrawView setDrawColor(int drawColor) {
    final int color = Color.rgb(Color.red(drawColor), Color.green(drawColor), Color.blue(drawColor));
    this.mDrawColor = color;
    this.mDrawAlpha = Color.alpha(drawColor);
    if (listeners != null && !listeners.isEmpty()) {
      for (final OnDrawViewListener onDrawViewListener : listeners) {
        onDrawViewListener.onDrawColorChanged(color);
      }
    }
    new DrawPreferences(getContext()).setDrawColor(color);
    return this;
  }

  /**
   * @param drawWidth
   *     the paint width used for the next action.
   *
   * @return this instance of the view.
   */
  public DrawView setDrawWidth(int drawWidth) {
    this.mDrawWidth = drawWidth;
    return this;
  }

  /**
   * @param drawingMode
   *     the DrawingMode to be used for the next action.
   *
   * @return this instance of the view.
   */
  public DrawView setDrawingMode(DrawingMode drawingMode) {
    this.mDrawingMode = drawingMode;
    new DrawPreferences(getContext()).setDrawMode(drawingMode);
    return this;
  }

  /**
   * @param drawingTool
   *     the DrawingTool to be used for the next action.
   *
   * @return this instance of the view.
   */
  public DrawView setDrawingTool(DrawingTool drawingTool) {
    this.mDrawingTool = drawingTool;
    new DrawPreferences(getContext()).setDrawTool(drawingTool);
    return this;
  }

  /**
   * @param paintStyle
   *     the style to be used by the current paint.
   *
   * @return this instance of the view.
   */
  public DrawView setPaintStyle(SerializablePaint.Style paintStyle) {
    this.mPaintStyle = paintStyle;
    new DrawPreferences(getContext()).setPaintStyle(paintStyle);
    return this;
  }

  /**
   * @param fontFamily
   *     the typeface used when drawing text.
   *
   * @return this instance of the view.
   */
  public DrawView setFontFamily(Typeface fontFamily) {
    this.mFontFamily = fontFamily;
    return this;
  }

  /**
   * @param fontSize
   *     the font size used when drawing text.
   *
   * @return this instance of the view.
   */
  public DrawView setFontSize(float fontSize) {
    final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
    this.mFontSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, fontSize, displayMetrics);
    return this;
  }

  /**
   * @param file
   *     the file containing the background image.
   */
  public void setBackgroundImageFile(final File file) {
    if (file == null || !file.exists()) {
      backgroundBitmap = null;
    } else {
      final int height = getHeight();
      final int width = getWidth();
      this.backgroundBitmap = FileUtils.getBitmapAccuratelyScaledToDisplay(file, height, width);
      getLayoutParams().height = height;
      getLayoutParams().width = width;
    }
    invalidate();
  }

  /**
   * @return {@code true} if the drawing has been changed by the user, {@code false} if it has not.
   */
  public boolean hasChanged() {
    return touched;
  }

  /**
   * @param bitmap
   *     the image to be displayed in the background layer.
   */
  public void setBackgroundImage(final Bitmap bitmap) {
    if (bitmap == null) {
      this.backgroundBitmap = null;
    } else {
      this.backgroundBitmap = Bitmap.createScaledBitmap(bitmap, getWidth(), getHeight(), false);
    }
    invalidate();
  }

  /**
   * @param visibility
   *     {@code true} if the zoom region should be displayed, {@code false} if it should be hidden.
   */
  private void showHideZoomRegionView(final int visibility) {
    if (mZoomRegionCardView.getAnimation() == null) {
      AlphaAnimation alphaAnimation = null;

      if (visibility == INVISIBLE && mZoomRegionCardView.getVisibility() == VISIBLE) {
        alphaAnimation = new AlphaAnimation(1f, 0f);
      } else if (visibility == VISIBLE && mZoomRegionCardView.getVisibility() == INVISIBLE) {
        alphaAnimation = new AlphaAnimation(0f, 1f);
      }

      if (alphaAnimation != null) {
        alphaAnimation.setDuration(300);
        alphaAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
        alphaAnimation.setAnimationListener(new Animation.AnimationListener() {
          @Override
          public void onAnimationStart(Animation animation) {
            if (visibility == VISIBLE) {
              mZoomRegionCardView.setVisibility(VISIBLE);
            }
          }

          @Override
          public void onAnimationEnd(Animation animation) {
            if (visibility == INVISIBLE) {
              mZoomRegionCardView.setVisibility(INVISIBLE);
            }

            mZoomRegionCardView.setAnimation(null);
          }

          @Override
          public void onAnimationRepeat(Animation animation) {

          }
        });
        mZoomRegionCardView.startAnimation(alphaAnimation);
      }
    }
  }

  /**
   * @return the image displayed in the background layer.
   */
  public Bitmap getBackgroundBitmap() {
    return backgroundBitmap;
  }

  private class ScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
      if (mZoomEnabled) {
        mFromZoomRegion = false;
        mZoomFactor *= detector.getScaleFactor();
        setZoomScale(mZoomFactor, detector.getFocusX(), detector.getFocusY());
      }
      return false;
    }
  }

  /**
   * @param zoomScale
   *     the level of zoom.
   * @param focusX
   *     the point on the X axis to use as focus.
   * @param focusY
   *     the point on the Y axis to use as focus.
   */
  public void setZoomScale(final float zoomScale, final float focusX, final float focusY) {
    mZoomFactor = zoomScale;
    mZoomFactor = Math.max(1f, Math.min(mZoomFactor, mMaxZoomFactor));
    mZoomFactor = mZoomFactor > mMaxZoomFactor ? mMaxZoomFactor : mZoomFactor < 1f ? 1f : mZoomFactor;
    mZoomCenterX = focusX / mZoomFactor + mCanvasClipBounds.left;
    mZoomCenterY = focusY / mZoomFactor + mCanvasClipBounds.top;

    if (mZoomFactor > 1f) {
      showHideZoomRegionView(VISIBLE);
    } else {
      showHideZoomRegionView(INVISIBLE);
    }

    invalidate();
  }

  /**
   * @return the current zoom level.
   */
  public float getZoomFactor() {
    return mZoomFactor;
  }

  private class GestureListener extends GestureDetector.SimpleOnGestureListener {
    @Override
    public boolean onDoubleTap(final MotionEvent e) {
      if (mZoomEnabled) {
        mFromZoomRegion = false;
        int animationOption = -1;

        if (mZoomFactor >= 1f && mZoomFactor < mMaxZoomFactor) {
          animationOption = 0;
        } else if (mZoomFactor <= mMaxZoomFactor && mZoomFactor > 1f) {
          animationOption = 1;
        }

        if (animationOption != -1) {
          ValueAnimator valueAnimator = null;

          if (animationOption == 0) {
            valueAnimator = ValueAnimator.ofFloat(mZoomFactor, mMaxZoomFactor);
          } else {
            float distance = mMaxZoomFactor - mZoomFactor;
            valueAnimator = ValueAnimator.ofFloat(mZoomFactor, distance);
          }

          valueAnimator.setDuration(300);
          valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
          valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
              mZoomFactor = (float) animation.getAnimatedValue();
              mZoomFactor = mZoomFactor < 1f ? 1 : mZoomFactor;
              mZoomCenterX = e.getX() / mZoomFactor + mCanvasClipBounds.left;
              mZoomCenterY = e.getY() / mZoomFactor + mCanvasClipBounds.top;

              if (mZoomFactor > 1f) {
                mZoomRegionCardView.setVisibility(VISIBLE);
              } else {
                mZoomRegionCardView.setVisibility(INVISIBLE);
              }

              invalidate();
            }
          });
          valueAnimator.start();
        }
      }
      return true;
    }
  }

  /**
   * @param onDrawViewListener
   *     a listener to be fired on draw events.
   */
  public void addOnDrawViewListener(OnDrawViewListener onDrawViewListener) {
    if (listeners == null) {
      listeners = new ArrayList<>();
    }
    listeners.add(onDrawViewListener);
  }

  /**
   * Listener for drawing events.
   */
  public interface OnDrawViewListener {

    /**
     * Fired when the user starts drawing.
     */
    void onStartDrawing();

    /**
     * Fired when the user finishes drawing.
     */
    void onEndDrawing();

    /**
     * Fired when the drawing is cleared.
     */
    void onClearDrawing();

    /**
     * Fired when text is requested.
     */
    void onRequestText();

    /**
     * Fired when all draw moves have been painted in onDraw().
     */
    void onAllMovesPainted();

    /**
     * Fired when the draw color has changed.
     *
     * @param drawColor
     *     the new draw color.
     */
    void onDrawColorChanged(final int drawColor);
  }
}
