/**
 * Copyright (c) 2016 UPTech
 *
 * 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.thoughtcrime.securesms.scribbles.widget;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.GestureDetectorCompat;
import android.support.v4.view.ViewCompat;
import android.text.Editable;
import android.text.InputType;
import android.text.Selection;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
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.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.FrameLayout;

import com.annimon.stream.Stream;

import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.scribbles.multitouch.MoveGestureDetector;
import org.thoughtcrime.securesms.scribbles.multitouch.RotateGestureDetector;
import org.thoughtcrime.securesms.scribbles.widget.entity.MotionEntity;
import org.thoughtcrime.securesms.scribbles.widget.entity.TextEntity;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

public class MotionView  extends FrameLayout implements TextWatcher {

  private static final String TAG = MotionView.class.getSimpleName();

  public interface Constants {
    float SELECTED_LAYER_ALPHA = 0.15F;
  }

  public interface MotionViewCallback {
    void onEntitySelected(@Nullable MotionEntity entity);
    void onEntityDoubleTap(@NonNull MotionEntity entity);
  }

  // layers
  private final List<MotionEntity> entities = new ArrayList<>();
  @Nullable
  private MotionEntity selectedEntity;

  private Paint selectedLayerPaint;

  // callback
  @Nullable
  private MotionViewCallback motionViewCallback;

  private EditText editText;

  // gesture detection
  private ScaleGestureDetector scaleGestureDetector;
  private RotateGestureDetector rotateGestureDetector;
  private MoveGestureDetector moveGestureDetector;
  private GestureDetectorCompat gestureDetectorCompat;

  // constructors
  public MotionView(Context context) {
    super(context);
    init(context, null);
  }

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

  public MotionView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context, attrs);
  }

  @SuppressWarnings("unused")
  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  public MotionView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    init(context, attrs);
  }

  private void init(@NonNull Context context, @Nullable AttributeSet attrs) {
    // I fucking love Android
    setWillNotDraw(false);

    selectedLayerPaint = new Paint();
    selectedLayerPaint.setAlpha((int) (255 * Constants.SELECTED_LAYER_ALPHA));
    selectedLayerPaint.setAntiAlias(true);

    this.editText = new EditText(context, attrs);
    ViewCompat.setAlpha(this.editText, 0);
    this.editText.setLayoutParams(new LayoutParams(1, 1, Gravity.TOP | Gravity.LEFT));
    this.editText.setClickable(false);
    this.editText.setBackgroundColor(Color.TRANSPARENT);
    this.editText.setTextSize(TypedValue.COMPLEX_UNIT_SP, 1);
    this.editText.setInputType(InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
    this.addView(editText);
    this.editText.clearFocus();
    this.editText.addTextChangedListener(this);
    this.editText.setId(R.id.motion_view_edittext);

    // init listeners
    this.scaleGestureDetector = new ScaleGestureDetector(context, new ScaleListener());
    this.rotateGestureDetector = new RotateGestureDetector(context, new RotateListener());
    this.moveGestureDetector = new MoveGestureDetector(context, new MoveListener());
    this.gestureDetectorCompat = new GestureDetectorCompat(context, new TapsListener());

    setOnTouchListener(onTouchListener);

    updateUI();
  }

  public SavedState saveState() {
    return new SavedState(entities);
  }

  public void restoreState(@NonNull SavedState savedState) {
    this.entities.clear();
    this.entities.addAll(savedState.getEntities());

    postInvalidate();
  }

  public void startEditing(TextEntity entity) {
    editText.setFocusableInTouchMode(true);
    editText.setFocusable(true);
    editText.requestFocus();
    editText.setText(entity.getLayer().getText());
    Selection.setSelection(editText.getText(), editText.length());

    InputMethodManager ims = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
    ims.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
  }

  public MotionEntity getSelectedEntity() {
    return selectedEntity;
  }

  public List<MotionEntity> getEntities() {
    return entities;
  }

  public void setMotionViewCallback(@Nullable MotionViewCallback callback) {
    this.motionViewCallback = callback;
  }

  public void addEntity(@Nullable MotionEntity entity) {
    if (entity != null) {
      entities.add(entity);
      selectEntity(entity, false);
    }
  }

  public void addEntityAndPosition(@Nullable MotionEntity entity) {
    if (entity != null) {
      initEntityBorder(entity);
      initialTranslateAndScale(entity);
      entities.add(entity);
      selectEntity(entity, true);
    }
  }

  public @NonNull Set<Integer> getUniqueColors() {
    Set<Integer> colors = new LinkedHashSet<>();

    for (MotionEntity entity : entities) {
      if (entity instanceof TextEntity) {
        colors.add(((TextEntity) entity).getLayer().getFont().getColor());
      }
    }

    return colors;
  }

  private void initEntityBorder(@NonNull MotionEntity entity ) {
    // init stroke
    int strokeSize = getResources().getDimensionPixelSize(R.dimen.scribble_stroke_size);
    Paint borderPaint = new Paint();
    borderPaint.setStrokeWidth(strokeSize);
    borderPaint.setAntiAlias(true);
    borderPaint.setColor(getContext().getResources().getColor(R.color.sticker_selected_color));

    entity.setBorderPaint(borderPaint);
  }

  @Override
  protected void dispatchDraw(Canvas canvas) {
    super.dispatchDraw(canvas);

    // dispatch draw is called after child views is drawn.
    // the idea that is we draw background stickers, than child views (if any), and than selected item
    // to draw on top of child views - do it in dispatchDraw(Canvas)
    // to draw below that - do it in onDraw(Canvas)
    if (selectedEntity != null) {
      selectedEntity.draw(canvas, selectedLayerPaint);
    }
  }

  @Override
  protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    render(canvas, entities);
  }

  public void render(Canvas canvas) {
    unselectEntity();
    draw(canvas);
  }

  public static void render(Canvas canvas, List<MotionEntity> entities) {
    for (int i = 0; i < entities.size(); i++) {
      entities.get(i).draw(canvas, null);
    }
  }

  /**
   * as a side effect - the method deselects Entity (if any selected)
   * @return bitmap with all the Entities at their current positions
   */
  public Bitmap getThumbnailImage() {
    selectEntity(null, false);

    Bitmap bmp = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
    // IMPORTANT: always create white background, cos if the image is saved in JPEG format,
    // which doesn't have transparent pixels, the background will be black
    bmp.eraseColor(Color.WHITE);
    Canvas canvas = new Canvas(bmp);
    render(canvas, entities);

    return bmp;
  }

  private void updateUI() {
    invalidate();
  }

  private void handleTranslate(PointF delta) {
    if (selectedEntity != null) {
      float newCenterX = selectedEntity.absoluteCenterX() + delta.x;
      float newCenterY = selectedEntity.absoluteCenterY() + delta.y;
      // limit entity center to screen bounds
      boolean needUpdateUI = false;
      if (newCenterX >= 0 && newCenterX <= getWidth()) {
        selectedEntity.getLayer().postTranslate(delta.x / getWidth(), 0.0F);
        needUpdateUI = true;
      }
      if (newCenterY >= 0 && newCenterY <= getHeight()) {
        selectedEntity.getLayer().postTranslate(0.0F, delta.y / getHeight());
        needUpdateUI = true;
      }
      if (needUpdateUI) {
        updateUI();
      }
    }
  }

  private void initialTranslateAndScale(@NonNull MotionEntity entity) {
    entity.moveToCanvasCenter();
    entity.getLayer().setScale(entity.getLayer().initialScale());
  }

  private void selectEntity(@Nullable MotionEntity entity, boolean updateCallback) {
    if (selectedEntity != null && entity != selectedEntity) {
      selectedEntity.setIsSelected(false);

      if (selectedEntity instanceof TextEntity) {
        if (TextUtils.isEmpty(((TextEntity) selectedEntity).getLayer().getText())) {
          deletedSelectedEntity();
        } else {
          editText.clearComposingText();
          editText.clearFocus();
        }
        InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
        imm.hideSoftInputFromWindow(editText.getWindowToken(), 0);
      }

    }
    if (entity != null) {
      entity.setIsSelected(true);
    }
    selectedEntity = entity;
    invalidate();
    if (updateCallback && motionViewCallback != null) {
      motionViewCallback.onEntitySelected(entity);
    }
  }

  public void unselectEntity() {
    if (selectedEntity != null) {
      selectEntity(null, false);
    }
  }

  @Nullable
  private MotionEntity findEntityAtPoint(float x, float y) {
    MotionEntity selected = null;
    PointF p = new PointF(x, y);
    for (int i = entities.size() - 1; i >= 0; i--) {
      if (entities.get(i).pointInLayerRect(p)) {
        selected = entities.get(i);
        break;
      }
    }
    return selected;
  }

  private void updateSelectionOnTap(MotionEvent e) {
    MotionEntity entity = findEntityAtPoint(e.getX(), e.getY());
    selectEntity(entity, true);
  }

  private void updateOnLongPress(MotionEvent e) {
    // if layer is currently selected and point inside layer - move it to front
    if (selectedEntity != null) {
      PointF p = new PointF(e.getX(), e.getY());
      if (selectedEntity.pointInLayerRect(p)) {
        bringLayerToFront(selectedEntity);
      }
    }
  }

  private void bringLayerToFront(@NonNull MotionEntity entity) {
    // removing and adding brings layer to front
    if (entities.remove(entity)) {
      entities.add(entity);
      invalidate();
    }
  }

  private void moveEntityToBack(@Nullable MotionEntity entity) {
    if (entity == null) {
      return;
    }
    if (entities.remove(entity)) {
      entities.add(0, entity);
      invalidate();
    }
  }

  public void flipSelectedEntity() {
    if (selectedEntity == null) {
      return;
    }
    selectedEntity.getLayer().flip();
    invalidate();
  }

  public void moveSelectedBack() {
    moveEntityToBack(selectedEntity);
  }

  public void deletedSelectedEntity() {
    if (selectedEntity == null) {
      return;
    }
    if (entities.remove(selectedEntity)) {
      selectedEntity.release();
      selectedEntity = null;
      invalidate();
    }
  }

  // memory
  public void release() {
    for (MotionEntity entity : entities) {
      entity.release();
    }
  }

  // gesture detectors

  private final View.OnTouchListener onTouchListener = new View.OnTouchListener() {

    @Override
    public boolean onTouch(View v, MotionEvent event) {
      if (scaleGestureDetector != null) {
        scaleGestureDetector.onTouchEvent(event);
        rotateGestureDetector.onTouchEvent(event);
        moveGestureDetector.onTouchEvent(event);
        gestureDetectorCompat.onTouchEvent(event);
      }
      return true;
    }
  };

  private class TapsListener extends GestureDetector.SimpleOnGestureListener {
    @Override
    public boolean onDoubleTap(MotionEvent e) {
      if (motionViewCallback != null && selectedEntity != null) {
        motionViewCallback.onEntityDoubleTap(selectedEntity);
      }
      return true;
    }

    @Override
    public void onLongPress(MotionEvent e) {
      updateOnLongPress(e);
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
      updateSelectionOnTap(e);
      return true;
    }

    @Override
    public boolean onDown(MotionEvent e) {
      updateSelectionOnTap(e);
      return false;
    }
  }

  private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
      if (selectedEntity != null) {
        float scaleFactorDiff = detector.getScaleFactor();
        Log.d(TAG, "ScaleFactorDiff: " + scaleFactorDiff);
        selectedEntity.getLayer().postScale(scaleFactorDiff - 1.0F);
        selectedEntity.updateEntity();
        updateUI();
      }
      return true;
    }
  }

  private class RotateListener extends RotateGestureDetector.SimpleOnRotateGestureListener {
    @Override
    public boolean onRotate(RotateGestureDetector detector) {
      if (selectedEntity != null) {
        selectedEntity.getLayer().postRotate(-detector.getRotationDegreesDelta());
        updateUI();
      }
      return true;
    }
  }

  private class MoveListener extends MoveGestureDetector.SimpleOnMoveGestureListener {
    @Override
    public boolean onMove(MoveGestureDetector detector) {
      handleTranslate(detector.getFocusDelta());
      return true;
    }
  }

  @Override
  public void beforeTextChanged(CharSequence s, int start, int count, int after) {}

  @Override
  public void onTextChanged(CharSequence s, int start, int before, int count) {}

  @Override
  public void afterTextChanged(Editable s) {
    String       text   = s.toString();
    MotionEntity entity = getSelectedEntity();

    if (entity != null && entity instanceof TextEntity) {
      TextEntity textEntity = (TextEntity)entity;

      if (!textEntity.getLayer().getText().equals(text)) {
        textEntity.getLayer().setText(text);
        textEntity.updateEntity();
        MotionView.this.invalidate();
      }
    }
  }

  static class SavedState {

    private final List<MotionEntity> entities;

    SavedState(List<MotionEntity> entities) {
      this.entities = new ArrayList<>(entities);
      Stream.of(entities).forEach(e -> e.setIsSelected(false));
    }

    List<MotionEntity> getEntities() {
      return entities;
    }

    boolean isEmpty() {
      return entities.isEmpty();
    }
  }
}
