/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.weex.utils;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BlurMaskFilter;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.support.annotation.IntRange;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import org.apache.weex.WXEnvironment;
import org.apache.weex.common.WXThread;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Created by moxun on 2017/9/4.
 * Utils for create shadow layer on view
 * Requires Android 4.3 and higher
 *
 * @see <a href="https://www.w3schools.com/cssref/css3_pr_box-shadow.asp">CSS3 box-shadow Property</>
 */

public class BoxShadowUtil {
  private static final String TAG = "BoxShadowUtil";
  private static boolean sBoxShadowEnabled = true /*disable box-shadow temporary*/;

  private static Pattern sColorPattern;

  public static void setBoxShadowEnabled(boolean enabled) {
    sBoxShadowEnabled = enabled;
    WXLogUtils.w(TAG, "Switch box-shadow status: " + enabled);
  }

  public static boolean isBoxShadowEnabled() {
    return sBoxShadowEnabled;
  }

  public static void setBoxShadow(final View target, String style, final float[] radii, int viewPort, final float quality) {
    if (!sBoxShadowEnabled) {
      WXLogUtils.w(TAG, "box-shadow was disabled by config");
      return;
    }

    if (target == null) {
      WXLogUtils.w(TAG, "Target view is null!");
      return;
    }

    if (TextUtils.isEmpty(style) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
      target.getOverlay().clear();
      WXLogUtils.d(TAG, "Remove all box-shadow");
      return;
    }

    final BoxShadowOptions[] shadows = parseBoxShadows(style, viewPort);
    if (shadows == null || shadows.length == 0) {
      WXLogUtils.w(TAG, "Failed to parse box-shadow: " + style);
      return;
    }

    final List<BoxShadowOptions> normalShadows = new ArrayList<>(), insetShadows = new ArrayList<>();
    for (BoxShadowOptions shadow : shadows) {
      if (shadow != null) {
        if (shadow.isInset) {
          insetShadows.add(0, shadow);
        } else {
          normalShadows.add(0, shadow);
        }
      }
    }

    if (radii != null) {
      if (radii.length != 8) {
        WXLogUtils.w(TAG, "Length of radii must be 8");
      } else {
        for (int i = 0; i < radii.length; i++) {
          float realRadius = WXViewUtils.getRealSubPxByWidth(radii[i], viewPort);
          radii[i] = realRadius;
        }
      }
    }

    target.post(WXThread.secure(new Runnable() {
      @Override
      public void run() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
          target.getOverlay().clear();
          if (normalShadows.size() > 0) {
            setNormalBoxShadow(target, normalShadows, quality, radii);
          }

          if (insetShadows.size() > 0) {
            setInsetBoxShadow(target, insetShadows, quality, radii);
          }
        }
      }
    }));
  }

  private static void drawShadow(Canvas canvas, BoxShadowOptions options) {
    RectF shadowRect = new RectF(
        0f, 0f,
        options.viewWidth + 2f * options.spread, options.viewHeight + 2f * options.spread
    );

    if (options.topLeft != null) {
      shadowRect.offset(options.topLeft.x, options.topLeft.y);
    }

    float shadowDx = options.blur;
    float shadowDy = options.blur;
    if (options.hShadow > 0) {
      shadowDx = shadowDx + 2f * options.hShadow;
    }
    if (options.vShadow > 0) {
      shadowDy = shadowDy + 2f * options.vShadow;
    }
    shadowRect.offset(shadowDx, shadowDy);

    Paint shadowPaint = new Paint();
    shadowPaint.setAntiAlias(true);
    shadowPaint.setColor(options.color);
    shadowPaint.setStyle(Paint.Style.FILL);

    if (options.blur > 0) {
      shadowPaint.setMaskFilter(new BlurMaskFilter(options.blur, BlurMaskFilter.Blur.NORMAL));
    }

    Path shadowPath = new Path();
    float[] shadowRadii = new float[8];
    for (int i = 0; i < options.radii.length; i++) {
      float contentRadius = options.radii[i];
      if (contentRadius == 0f) {
        shadowRadii[i] = 0f;
      } else {
        shadowRadii[i] = options.radii[i] + options.spread;
      }
    }
    shadowPath.addRoundRect(shadowRect, shadowRadii, Path.Direction.CCW);
    canvas.drawPath(shadowPath, shadowPaint);

    if (false && WXEnvironment.isApkDebugable()) {
      Paint paint = new Paint();
      paint.setAntiAlias(true);
      paint.setColor(Color.BLACK);
      paint.setStyle(Paint.Style.STROKE);
      canvas.drawRect(shadowRect, paint);
    }
  }

  private static void setNormalBoxShadow(View target, List<BoxShadowOptions> options, float quality, float[] radii) {
    int h = target.getHeight();
    int w = target.getWidth();

    ViewGroup.LayoutParams p = target.getLayoutParams();

    if (h == 0 || w == 0) {
      Log.w(TAG, "Target view is invisible, ignore set shadow.");
      return;
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
      int maxWidth = 0, maxHeight = 0;
      for (BoxShadowOptions option : options) {
        option.viewWidth = w;
        option.viewHeight = h;
        option.radii = radii;

        Rect rect = option.getTargetCanvasRect();
        if (maxWidth < rect.width()) {
          maxWidth = rect.width();
        }

        if (maxHeight < rect.height()) {
          maxHeight = rect.height();
        }
      }

      int canvasWidth = (int) (maxWidth * quality);
      int canvasHeight = (int) (maxHeight * quality);
      Bitmap output = Bitmap.createBitmap(canvasWidth, canvasHeight, Bitmap.Config.ARGB_4444);
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        WXLogUtils.d(TAG, "Allocation memory for box-shadow: " + (output.getAllocationByteCount() / 1024) + " KB");
      }
      Canvas canvas = new Canvas(output);

      if (false && WXEnvironment.isApkDebugable()) {
        // Using for debug
        Paint strokePaint = new Paint();
        strokePaint.setColor(Color.BLACK);
        strokePaint.setStrokeWidth(2);
        strokePaint.setStyle(Paint.Style.STROKE);
        canvas.drawRect(canvas.getClipBounds(), strokePaint);
      }

      for (BoxShadowOptions option : options) {
        Rect rect = option.getTargetCanvasRect();
        float left = (maxWidth - rect.width()) / 2f;
        float top = (maxHeight - rect.height()) / 2f;
        option.topLeft = new PointF(left, top);

        BoxShadowOptions scaledOption = option.scale(quality);
        drawShadow(canvas, scaledOption);
      }

      //Drawable's bounds must match the bitmap size, otherwise the shadows will be scaled
      int paddingX = (maxWidth - w) / 2;
      int paddingY = (maxHeight - h) / 2;
      OverflowBitmapDrawable shadowDrawable = new OverflowBitmapDrawable(target.getResources(),
          output, new Point(paddingX, paddingY), new Rect(0, 0, w, h), radii);

      target.getOverlay().add(shadowDrawable);
      //Relayout to ensure the shadows are fully drawn
      ViewParent parent = target.getParent();
      if (parent != null) {
        parent.requestLayout();
        if (parent instanceof ViewGroup) {
          ((ViewGroup) parent).invalidate(shadowDrawable.getBounds());
        }
      }
    } else {
      // I have a dream that one day our minSdkVersion will equals or higher than 21
      Log.w("BoxShadowUtil", "Call setNormalBoxShadow() requires API level 18 or higher.");
    }
  }

  private static void setInsetBoxShadow(View target, List<BoxShadowOptions> options, float quality, float[] radii) {
    if (target == null || options == null) {
      WXLogUtils.w(TAG, "Illegal arguments");
      return;
    }

    if (target.getWidth() == 0 || target.getHeight() == 0) {
      WXLogUtils.w(TAG, "Target view is invisible, ignore set shadow.");
      return;
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
      Drawable[] drawables = new Drawable[options.size()];
      for (int i = 0; i < options.size(); i++) {
        BoxShadowOptions option = options.get(i);
        Drawable shadow = new InsetShadowDrawable(target.getWidth(), target.getHeight(),
            option.hShadow, option.vShadow,
            option.blur, option.spread,
            option.color, radii);
        drawables[i] = shadow;
      }

      LayerDrawable layerDrawable = new LayerDrawable(drawables);
      target.getOverlay().add(layerDrawable);
      target.invalidate();
    } else {
      Log.w(TAG, "Call setInsetBoxShadow() requires API level 18 or higher.");
    }
  }

  public static BoxShadowOptions[] parseBoxShadows(String boxShadowStyle, int viewport) {
    // normalization color expression to #AARRGGBB
    if (sColorPattern == null) {
      sColorPattern = Pattern.compile("([rR][gG][bB][aA]?)\\((\\d+\\s*),\\s*(\\d+\\s*),\\s*(\\d+\\s*)(?:,\\s*(\\d+(?:\\.\\d+)?))?\\)");
    }

    Matcher matcher = sColorPattern.matcher(boxShadowStyle);

    String processedStyle = boxShadowStyle;
    while (matcher.find()) {
      String color = matcher.group();
      processedStyle = processedStyle.replace(color, "#" + String.format("%8s", Integer.toHexString(WXResourceUtils.getColor(color, Color.BLACK))).replaceAll("\\s", "0"));
    }

    String[] styles = processedStyle.split(",");
    if (styles != null && styles.length > 0) {
      BoxShadowOptions[] result = new BoxShadowOptions[styles.length];
      for (int i = 0; i < styles.length; i++) {
        result[i] = parseBoxShadow(styles[i], viewport);
      }
      return result;
    }
    return null;
  }

  private static BoxShadowOptions parseBoxShadow(String boxShadow, int viewport) {
    BoxShadowOptions result = new BoxShadowOptions(viewport);
    if (TextUtils.isEmpty(boxShadow)) {
      return null;
    }

    String boxShadowCopy = boxShadow;

    // trim rgb() & rgba()
    boxShadowCopy = boxShadowCopy.replaceAll("\\s*,\\s+", ",");

    // match inset first
    if (boxShadowCopy.contains("inset")) {
      result.isInset = true;
      boxShadowCopy = boxShadowCopy.replace("inset", "");
    }

    boxShadowCopy = boxShadowCopy.trim();
    List<String> params = new ArrayList<>(Arrays.asList(boxShadowCopy.split("\\s+")));

    // match color
    String maybeColor = params.get(params.size() - 1);
    if (!TextUtils.isEmpty(maybeColor)) {
      if (maybeColor.startsWith("#") || maybeColor.startsWith("rgb") || WXResourceUtils.isNamedColor(maybeColor)) {
        result.color = WXResourceUtils.getColor(maybeColor, Color.BLACK);
        params.remove(params.size() - 1);
      }
    }

    try {
      if (params.size() < 2) {
        // Missing required param
        return null;
      } else {
        if (!TextUtils.isEmpty(params.get(0))) {
          float px = WXUtils.getFloat(params.get(0).trim(), 0f);
          result.hShadow = WXViewUtils.getRealSubPxByWidth(px, viewport);
        }

        if (!TextUtils.isEmpty(params.get(1))) {
          float px = WXUtils.getFloat(params.get(1).trim(), 0f);
          result.vShadow = WXViewUtils.getRealPxByWidth(px, viewport);
        }

        for (int i = 2; i < params.size(); i++) {
          int parserIndex = i - 2;
          BoxShadowOptions.IParser parser = result.optionParamParsers.get(parserIndex);
          parser.parse(params.get(i));
        }
      }
    } catch (Throwable t) {
      t.printStackTrace();
    }
    return result;
  }

  private static class OverflowBitmapDrawable extends BitmapDrawable {
    private int paddingX;
    private int paddingY;
    private Rect viewRect;
    private float[] radii;

    private OverflowBitmapDrawable(Resources resources, Bitmap bitmap, Point topLeft, Rect viewRect, float[] radii) {
      super(resources, bitmap);
      this.paddingX = topLeft.x;
      this.paddingY = topLeft.y;
      this.viewRect = viewRect;
      this.radii = radii;

      setBounds(-paddingX, -paddingY, viewRect.width() + paddingX, viewRect.height() + paddingY);
    }

    public void draw(Canvas canvas) {
      Rect bounds = canvas.getClipBounds();
      Rect newRect = new Rect(bounds);
      // Make the Canvas Rect bigger according to the padding.
      newRect.inset(-paddingX * 2, -paddingY * 2);
      try {
        if(WXEnvironment.sApplication.getApplicationInfo().targetSdkVersion > VERSION_CODES.O){
          canvas.clipRect(newRect);
        }
        else{
          canvas.clipRect(newRect, Region.Op.REPLACE);
        }
      }catch (NullPointerException e) {
        canvas.clipRect(newRect);
      }

      Path contentPath = new Path();
      // the content area map must be aligned with bounds
      RectF rectF = new RectF(bounds);
      contentPath.addRoundRect(rectF, radii, Path.Direction.CCW);
      // can not antialias
      canvas.clipPath(contentPath, Region.Op.DIFFERENCE);

      // translate the canvas to a suitable position and then draw the bitmap, otherwise draw from the origin(0, 0)
      canvas.translate(bounds.left, bounds.top);

      super.draw(canvas);
    }
  }

  private static class InsetShadowDrawable extends Drawable {
    private static final int LEFT_TO_RIGHT = 0;
    private static final int TOP_TO_BOTTOM = 1;
    private static final int RIGHT_TO_LEFT = 2;
    private static final int BOTTOM_TO_TOP = 3;

    private float blurRadius;
    private int shadowColor;

    private float[] radii;

    private float width, height;

    private float shadowXSize, shadowYSize;

    private Shader[] shades = new Shader[4];
    private Path[] paths = new Path[4];

    private Paint paint;

    private InsetShadowDrawable(int viewWidth, int viewHeight, float dx, float dy, float blurRadius, float spread, int shadowColor, float[] radii) {
      this.blurRadius = blurRadius;
      this.shadowColor = shadowColor;

      this.width = viewWidth + 2 * dx;
      this.height = viewHeight + 2 * dy;

      this.shadowXSize = dx + spread;
      this.shadowYSize = dy + spread;

      this.radii = radii;

      setBounds(0, 0, viewWidth, viewHeight);
      prepare();
    }

    private void prepare() {
      /*
       *  A +++++++++++++++++++++++++ B
       *  +  E ------------------- F  +
       *  +  |                     |  +
       *  +  |                     |  +
       *  +  |                     |  +
       *  +  H ------------------- G  +
       *  D +++++++++++++++++++++++++ C
       */

      PointF a = new PointF(0, 0);
      PointF b = new PointF(width, 0);
      PointF c = new PointF(b.x, height);
      PointF d = new PointF(a.x, c.y);

      PointF e = new PointF(shadowXSize, shadowYSize);
      PointF f = new PointF(b.x - shadowXSize, e.y);
      PointF g = new PointF(f.x, c.y - shadowYSize);
      PointF h = new PointF(e.x, g.y);

      Shader ltr = new LinearGradient(e.x - blurRadius, e.y, e.x, e.y, shadowColor, Color.TRANSPARENT, Shader.TileMode.CLAMP);
      Shader ttb = new LinearGradient(e.x, e.y - blurRadius, e.x, e.y, shadowColor, Color.TRANSPARENT, Shader.TileMode.CLAMP);
      Shader rtl = new LinearGradient(g.x + blurRadius, g.y, g.x, g.y, shadowColor, Color.TRANSPARENT, Shader.TileMode.CLAMP);
      Shader btt = new LinearGradient(g.x, g.y + blurRadius, g.x, g.y, shadowColor, Color.TRANSPARENT, Shader.TileMode.CLAMP);

      shades[LEFT_TO_RIGHT] = ltr;
      shades[TOP_TO_BOTTOM] = ttb;
      shades[RIGHT_TO_LEFT] = rtl;
      shades[BOTTOM_TO_TOP] = btt;

      Path ltrPath = new Path();
      ltrPath.moveTo(a.x, a.y);
      ltrPath.lineTo(e.x, e.y);
      ltrPath.lineTo(h.x, h.y);
      ltrPath.lineTo(d.x, d.y);
      ltrPath.close();

      Path ttbPath = new Path();
      ttbPath.moveTo(a.x, a.y);
      ttbPath.lineTo(b.x, b.y);
      ttbPath.lineTo(f.x, f.y);
      ttbPath.lineTo(e.x, e.y);
      ttbPath.close();

      Path rtlPath = new Path();
      rtlPath.moveTo(b.x, b.y);
      rtlPath.lineTo(c.x, c.y);
      rtlPath.lineTo(g.x, g.y);
      rtlPath.lineTo(f.x, f.y);
      rtlPath.close();

      Path bttPath = new Path();
      bttPath.moveTo(d.x, d.y);
      bttPath.lineTo(c.x, c.y);
      bttPath.lineTo(g.x, g.y);
      bttPath.lineTo(h.x, h.y);
      bttPath.close();

      paths[LEFT_TO_RIGHT] = ltrPath;
      paths[TOP_TO_BOTTOM] = ttbPath;
      paths[RIGHT_TO_LEFT] = rtlPath;
      paths[BOTTOM_TO_TOP] = bttPath;

      paint = new Paint();
      paint.setAntiAlias(true);
      paint.setStyle(Paint.Style.FILL);
      paint.setColor(shadowColor);
    }

    @Override
    public void draw(Canvas canvas) {
      Rect bounds = canvas.getClipBounds();
      Path border = new Path();
      RectF rectF = new RectF(bounds);
      border.addRoundRect(rectF, radii, Path.Direction.CCW);
      canvas.clipPath(border);
      //TODO: we need clip border-width too

      // translate the canvas to the right place and then draw the inner shadow
      canvas.translate(bounds.left, bounds.top);

      for (int i = 0; i < 4; i++) {
        Shader shader = shades[i];
        Path path = paths[i];
        paint.setShader(shader);
        canvas.drawPath(path, paint);
      }
    }

    @Override
    public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {

    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {

    }

    @Override
    public int getOpacity() {
      return PixelFormat.OPAQUE;
    }
  }

  public static class BoxShadowOptions {
    private List<IParser> optionParamParsers;
    private int viewport = 750;

    public float hShadow;
    public float vShadow;
    public float blur = 0f;
    public float spread = 0f;
    public float[] radii = new float[]{0, 0, 0, 0, 0, 0, 0, 0};
    public int color = Color.BLACK;
    public boolean isInset = false;

    public int viewWidth = 0;
    public int viewHeight = 0;
    public PointF topLeft = null;

    private BoxShadowOptions(int vp) {
      if (viewport != 0) {
        this.viewport = vp;
      }
      optionParamParsers = new ArrayList<>();

      IParser spreadParser = new IParser() {
        @Override
        public void parse(String param) {
          if (!TextUtils.isEmpty(param)) {
            float px = WXUtils.getFloat(param, 0f);
            spread = WXViewUtils.getRealSubPxByWidth(px, viewport);
            WXLogUtils.w(TAG, "Experimental box-shadow attribute: spread");
          }
        }
      };

      IParser blurParser = new IParser() {
        @Override
        public void parse(String param) {
          if (!TextUtils.isEmpty(param)) {
            float px = WXUtils.getFloat(param, 0f);
            blur = WXViewUtils.getRealSubPxByWidth(px, viewport);
          }
        }
      };

      optionParamParsers.add(blurParser);
      optionParamParsers.add(spreadParser);
    }

    public BoxShadowOptions scale(float scale) {
      if (scale > 0f && scale <= 1f) {
        BoxShadowOptions scaledOptions = new BoxShadowOptions(viewport);
        scaledOptions.hShadow = hShadow * scale;
        scaledOptions.vShadow = vShadow * scale;
        scaledOptions.blur = blur * scale;
        scaledOptions.spread = spread * scale;
        for (int i = 0; i < radii.length ; i++) {
          scaledOptions.radii[i] = radii[i] * scale;
        }
        scaledOptions.viewHeight = (int) (viewHeight * scale);
        scaledOptions.viewWidth = (int) (viewWidth * scale);

        if (topLeft != null) {
          scaledOptions.topLeft = new PointF();
          scaledOptions.topLeft.x = topLeft.x * scale;
          scaledOptions.topLeft.y = topLeft.y * scale;
        }

        scaledOptions.color = color;
        scaledOptions.isInset = isInset;
        WXLogUtils.d(TAG, "Scaled BoxShadowOptions: [" + scale + "] " + scaledOptions);
        return scaledOptions;
      }
      return null;
    }

    public Rect getTargetCanvasRect() {
      int canvasWidth = viewWidth + 2 * (int) (blur + spread + Math.abs(hShadow));
      int canvasHeight = viewHeight + 2 * (int) (blur + spread + Math.abs(vShadow));
      return new Rect(0, 0, canvasWidth, canvasHeight);
    }

    @Override
    public String toString() {

      String r = "[" + radii[0] + "," + radii[2] + "," + radii[4] + "," + radii[6] + "]";

      final StringBuffer sb = new StringBuffer("BoxShadowOptions{");
      sb.append("h-shadow=").append(hShadow);
      sb.append(", v-shadow=").append(vShadow);
      sb.append(", blur=").append(blur);
      sb.append(", spread=").append(spread);
      sb.append(", corner-radius=").append(r);
      sb.append(", color=#").append(Integer.toHexString(color));
      sb.append(", inset=").append(isInset);
      sb.append('}');
      return sb.toString();
    }

    private interface IParser {
      void parse(String param);
    }
  }
}
