/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed 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 com.android.ide.common.vectordrawable;

import com.google.common.collect.ImmutableMap;

import org.w3c.dom.NamedNodeMap;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Used to represent one VectorDrawble's path element.
 */
class VdPath extends VdElement{
    private static final Logger LOGGER = Logger.getLogger(VdPath.class.getSimpleName());

    private static final String PATH_ID = "android:name";
    private static final String PATH_DESCRIPTION = "android:pathData";
    private static final String PATH_FILL = "android:fillColor";
    private static final String PATH_FILL_OPACITY = "android:fillAlpha";
    private static final String PATH_FILL_TYPE = "android:fillType";
    private static final String PATH_STROKE = "android:strokeColor";
    private static final String PATH_STROKE_OPACTIY = "android:strokeAlpha";

    private static final String FILL_TYPE_EVEN_ODD = "evenOdd";

    private static final String PATH_STROKE_WIDTH = "android:strokeWidth";
    private static final String PATH_TRIM_START = "android:trimPathStart";
    private static final String PATH_TRIM_END = "android:trimPathEnd";
    private static final String PATH_TRIM_OFFSET = "android:trimPathOffset";
    private static final String PATH_STROKE_LINECAP = "android:strokeLineCap";
    private static final String PATH_STROKE_LINEJOIN = "android:strokeLineJoin";
    private static final String PATH_STROKE_MITERLIMIT = "android:strokeMiterLimit";

    private static final String LINECAP_BUTT = "butt";
    private static final String LINECAP_ROUND = "round";
    private static final String LINECAP_SQUARE = "square";
    private static final String LINEJOIN_MITER = "miter";
    private static final String LINEJOIN_ROUND = "round";
    private static final String LINEJOIN_BEVEL = "bevel";

    private Node[] mNodeList = null;
    private int mStrokeColor = 0;
    private int mFillColor = 0;

    private float mStrokeWidth = 0;
    private int mStrokeLineCap = 0;
    private int mStrokeLineJoin = 0;
    private float mStrokeMiterlimit = 4;
    private float mStrokeAlpha = 1.0f;
    private float mFillAlpha = 1.0f;
    private int mFillType = PathIterator.WIND_NON_ZERO;
    // TODO: support trim path.
    private float mTrimPathStart = 0;
    private float mTrimPathEnd = 1;
    private float mTrimPathOffset = 0;

    private void toPath(Path2D path) {
        path.reset();
        if (mNodeList != null) {
            VdNodeRender.createPath(mNodeList, path);
        }
    }

    /**
     * Represent one segment of the path data. Like "l 0,0 1,1"
     */
    public static class Node {
        private char mType;
        private float[] mParams;

        public char getType() {
            return mType;
        }

        public float[] getParams() {
            return mParams;
        }

        public Node(char type, float[] params) {
            this.mType = type;
            this.mParams = params;
        }

        public Node(Node n) {
            this.mType = n.mType;
            this.mParams = Arrays.copyOf(n.mParams, n.mParams.length);
        }

        public static boolean hasRelMoveAfterClose(Node[] nodes) {
            char preType = ' ';
            for (Node n : nodes) {
                if ((preType == 'z' || preType == 'Z') && n.mType == 'm') {
                    return true;
                }
                preType = n.mType;
            }
            return false;
        }

        public static String NodeListToString(Node[] nodes, String decimalPlaceString) {
            StringBuilder stringBuilder = new StringBuilder();
            for (int i = 0; i < nodes.length; i++) {
                Node n = nodes[i];
                stringBuilder.append(n.mType);
                int len = n.mParams.length;
                boolean implicitLineTo = false;
                char lineToType = ' ';
                if ((n.mType == 'm' || n.mType == 'M') && len > 2) {
                    implicitLineTo = true;
                    lineToType = n.mType == 'm' ? 'l' : 'L';
                }
                for (int j = 0; j < len; j++) {
                    if (j > 0) {
                        stringBuilder.append(((j & 1) == 1) ? "," : " ");
                    }
                    if (implicitLineTo && j == 2) {
                        stringBuilder.append(lineToType);
                    }
                    // To avoid trailing zeros like 17.0, use this trick
                    float value = n.mParams[j];
                    if (value == (long) value) {
                        stringBuilder.append(String.valueOf((long) value));
                    } else {
                        DecimalFormatSymbols fractionSeparator = new DecimalFormatSymbols();
                        fractionSeparator.setDecimalSeparator('.');
                        DecimalFormat df = new DecimalFormat(decimalPlaceString, fractionSeparator);
                        df.setRoundingMode(RoundingMode.HALF_UP);
                        stringBuilder.append(df.format(value));
                    }

                }
            }

            return stringBuilder.toString();
        }

        private static final char INIT_TYPE = ' ';
        public static void transform(AffineTransform totalTransform,
                                     Node[] nodes) {
            Point2D.Float currentPoint = new Point2D.Float();
            Point2D.Float currentSegmentStartPoint = new Point2D.Float();
            char previousType = INIT_TYPE;
            for (int i = 0; i < nodes.length; i++) {
                nodes[i].transform(totalTransform, currentPoint, currentSegmentStartPoint, previousType);
                previousType= nodes[i].mType;
            }
        }

        private static final ImmutableMap<Character, Integer> commandStepMap =
          ImmutableMap.<Character, Integer>builder()
            .put('z', 2)
            .put('Z', 2)
            .put('m', 2)
            .put('M', 2)
            .put('l', 2)
            .put('L', 2)
            .put('t', 2)
            .put('T', 2)
            .put('h', 1)
            .put('H', 1)
            .put('v', 1)
            .put('V', 1)
            .put('c', 6)
            .put('C', 6)
            .put('s', 4)
            .put('S', 4)
            .put('q', 4)
            .put('Q', 4)
            .put('a', 7)
            .put('A', 7)
            .build();

        private void transform(AffineTransform totalTransform, Point2D.Float currentPoint,
                               Point2D.Float currentSegmentStartPoint, char previousType) {
            // For Horizontal / Vertical lines, we have to convert to LineTo with 2 parameters
            // And for arcTo, we also need to isolate the parameters for transformation.
            // Therefore a looping will be necessary for such commands.
            //
            // Note that if the matrix is translation only, then we can save many computations.

            int paramsLen = mParams.length;
            float[] tempParams = new float[2 * paramsLen];
            // These has to be pre-transformed value. In another word, the same as it is
            // in the pathData.
            float currentX = currentPoint.x;
            float currentY = currentPoint.y;
            float currentSegmentStartX = currentSegmentStartPoint.x;
            float currentSegmentStartY = currentSegmentStartPoint.y;

            int step = commandStepMap.get(mType);
            switch (mType) {
                case 'z':
                case 'Z':
                    currentX = currentSegmentStartX;
                    currentY = currentSegmentStartY;
                    break;
                case 'M':
                case 'L':
                case 'T':
                case 'C':
                case 'S':
                case 'Q':
                    currentX = mParams[paramsLen - 2];
                    currentY = mParams[paramsLen - 1];
                    if (mType == 'M') {
                        currentSegmentStartX = currentX;
                        currentSegmentStartY = currentY;
                    }

                    totalTransform.transform(mParams, 0, mParams, 0, paramsLen / 2);
                    break;
                case 'm':
                    // We also need to workaround a bug in API 21 that 'm' after 'z'
                    // is not picking up the relative value correctly.
                    if (previousType == 'z' || previousType == 'Z') {
                        mType = 'M';
                        mParams[0] += currentSegmentStartX;
                        mParams[1] += currentSegmentStartY;
                        currentSegmentStartX = mParams[0];
                        currentSegmentStartY = mParams[1];
                        for (int i = 1; i < paramsLen / step; i++) {
                            mParams[i * step + 0] += mParams[(i - 1) * step + 0];
                            mParams[i * step + 1] += mParams[(i - 1) * step + 1];
                        }
                        currentX = mParams[paramsLen - 2];
                        currentY = mParams[paramsLen - 1];

                        totalTransform.transform(mParams, 0, mParams, 0, paramsLen / 2);
                    } else {

                        // We need to handle the initial 'm' similar to 'M' for first pair.
                        // Then all the following numbers are handled as 'l'
                        int startIndex = 0;
                        if (previousType == INIT_TYPE) {
                            int paramsLenInitialM = 2;
                            currentX = mParams[paramsLenInitialM - 2];
                            currentY = mParams[paramsLenInitialM - 1];
                            currentSegmentStartX = currentX;
                            currentSegmentStartY = currentY;

                            totalTransform.transform(mParams, 0, mParams, 0, paramsLenInitialM / 2);
                            startIndex = 1;
                        }
                        for (int i = startIndex; i < paramsLen / step; i++) {
                            int indexX = i * step + (step - 2);
                            int indexY = i * step + (step - 1);
                            currentX += mParams[indexX];
                            currentY += mParams[indexY];
                        }

                        if (!isTranslationOnly(totalTransform)) {
                            deltaTransform(totalTransform, mParams, 2 * startIndex,
                                    paramsLen - 2 * startIndex);
                        }
                    }

                    break;
                case 'l':
                case 't':
                case 'c':
                case 's':
                case 'q':
                    for (int i = 0; i < paramsLen / step; i ++) {
                        int indexX = i * step + (step - 2);
                        int indexY = i * step + (step - 1);
                        currentX += mParams[indexX];
                        currentY += mParams[indexY];
                    }
                    if (!isTranslationOnly(totalTransform)) {
                        deltaTransform(totalTransform, mParams, 0, paramsLen);
                    }
                    break;
                case 'H':
                    mType = 'L';
                    for (int i = 0; i < paramsLen; i ++) {
                        tempParams[i * 2 + 0] = mParams[i];
                        tempParams[i * 2 + 1] = currentY;
                        currentX = mParams[i];
                    }
                    totalTransform.transform(tempParams, 0, tempParams, 0, paramsLen /*points*/);
                    mParams = tempParams;
                    break;
                case 'V':
                    mType = 'L';
                    for (int i = 0; i < paramsLen; i ++) {
                        tempParams[i * 2 + 0] = currentX;
                        tempParams[i * 2 + 1] = mParams[i];
                        currentY = mParams[i];
                    }
                    totalTransform.transform(tempParams, 0, tempParams, 0, paramsLen /*points*/);
                    mParams = tempParams;
                    break;
                case 'h':
                    for (int i = 0; i < paramsLen; i ++) {
                        // tempParams may not be used, but I would rather merge the code here.
                        tempParams[i * 2 + 0] = mParams[i];
                        currentX += mParams[i];
                        tempParams[i * 2 + 1] = 0;
                    }
                    if (!isTranslationOnly(totalTransform)) {
                        mType = 'l';
                        deltaTransform(totalTransform, tempParams, 0, 2 * paramsLen);
                        mParams = tempParams;
                    }
                    break;
                case 'v':
                    for (int i = 0; i < paramsLen; i++) {
                        // tempParams may not be used, but I would rather merge the code here.
                        tempParams[i * 2 + 0] = 0;
                        tempParams[i * 2 + 1] = mParams[i];
                        currentY += mParams[i];
                    }

                    if (!isTranslationOnly(totalTransform)) {
                        mType = 'l';
                        deltaTransform(totalTransform, tempParams, 0, 2 * paramsLen);
                        mParams = tempParams;
                    }
                    break;
                case 'A':
                    for (int i = 0; i < paramsLen / step; i ++) {
                        // (0:rx 1:ry 2:x-axis-rotation 3:large-arc-flag 4:sweep-flag 5:x 6:y)
                        // [0, 1, 2]
                        if (!isTranslationOnly(totalTransform)) {
                            EllipseSolver ellipseSolver = new EllipseSolver(totalTransform,
                                    currentX, currentY,
                                    mParams[i * step + 0], mParams[i * step + 1], mParams[i * step + 2],
                                    mParams[i * step + 3], mParams[i * step + 4],
                                    mParams[i * step + 5], mParams[i * step + 6]);
                            mParams[i * step + 0] = ellipseSolver.getMajorAxis();
                            mParams[i * step + 1] = ellipseSolver.getMinorAxis();
                            mParams[i * step + 2] = ellipseSolver.getRotationDegree();
                            if (ellipseSolver.getDirectionChanged()) {
                                mParams[i * step + 4] = 1 - mParams[i * step + 4];
                            }
                        } else {
                            // No need to change the value of rx , ry, rotation, and flags.
                        }
                        // [5, 6]
                        currentX = mParams[i * step + 5];
                        currentY = mParams[i * step + 6];

                        totalTransform.transform(mParams, i * step + 5, mParams, i * step + 5, 1 /*1 point only*/);
                    }
                    break;
                case 'a':
                    for (int i = 0; i < paramsLen / step; i ++) {
                        float oldCurrentX = currentX;
                        float oldCurrentY = currentY;

                        currentX += mParams[i * step + 5];
                        currentY += mParams[i * step + 6];
                        if (!isTranslationOnly(totalTransform)) {
                            EllipseSolver ellipseSolver = new EllipseSolver(totalTransform,
                                    oldCurrentX, oldCurrentY,
                                    mParams[i * step + 0], mParams[i * step + 1], mParams[i * step + 2],
                                    mParams[i * step + 3], mParams[i * step + 4],
                                    oldCurrentX + mParams[i * step + 5],
                                    oldCurrentY + mParams[i * step + 6]);
                            // (0:rx 1:ry 2:x-axis-rotation 3:large-arc-flag 4:sweep-flag 5:x 6:y)
                            // [5, 6]
                            deltaTransform(totalTransform, mParams, i * step + 5, 2);
                            // [0, 1, 2]
                            mParams[i * step + 0] = ellipseSolver.getMajorAxis();
                            mParams[i * step + 1] = ellipseSolver.getMinorAxis();
                            mParams[i * step + 2] = ellipseSolver.getRotationDegree();
                            if (ellipseSolver.getDirectionChanged()) {
                                mParams[i * step + 4] = 1 - mParams[i * step + 4];
                            }
                        }

                    }
                    break;
                default:
                    throw new IllegalArgumentException("Type is not right!!!");
            }
            currentPoint.setLocation(currentX, currentY);
            currentSegmentStartPoint.setLocation(currentSegmentStartX, currentSegmentStartY);
        }

        private static boolean isTranslationOnly(AffineTransform totalTransform) {
            int type = totalTransform.getType();
            return type == AffineTransform.TYPE_IDENTITY
                    || type == AffineTransform.TYPE_TRANSLATION;
        }

        /**
         * Convert the <code>tempParams</code> into a double array, then apply the
         * delta transform and convert it back to float array.
         * @param offset in number of floats, not points.
         * @param paramsLen in number of floats, not points.
         */
        private static void deltaTransform(AffineTransform totalTransform, float[] tempParams,
                int offset, int paramsLen) {
            double[] doubleArray = new double[paramsLen];
            for (int i = 0; i < paramsLen; i++)
            {
                doubleArray[i] = (double) tempParams[i + offset];
            }

            totalTransform.deltaTransform(doubleArray, 0, doubleArray, 0, paramsLen / 2);

            for (int i = 0; i < paramsLen; i++)
            {
                tempParams[i + offset] = (float) doubleArray[i];
            }
        }
    }

    /**
     * @return color value in #AARRGGBB format.
     */
    private static int calculateColor(String value) {
        int len = value.length();
        int ret;
        int k = 0;
        switch (len) {
            case 7: // #RRGGBB
                ret = (int) Long.parseLong(value.substring(1), 16);
                ret |= 0xFF000000;
                break;
            case 9: // #AARRGGBB
                ret = (int) Long.parseLong(value.substring(1), 16);
                break;
            case 4: // #RGB
                ret = (int) Long.parseLong(value.substring(1), 16);

                k |= ((ret >> 8) & 0xF) * 0x110000;
                k |= ((ret >> 4) & 0xF) * 0x1100;
                k |= ((ret) & 0xF) * 0x11;
                ret = k | 0xFF000000;
                break;
            case 5: // #ARGB
                ret = (int) Long.parseLong(value.substring(1), 16);
                k |= ((ret >> 12) & 0xF) * 0x11000000;
                k |= ((ret >> 8) & 0xF) * 0x110000;
                k |= ((ret >> 4) & 0xF) * 0x1100;
                k |= ((ret) & 0xF) * 0x11;
                ret = k;
                break;
            default:
                return 0xFF000000;
        }
        return ret;
    }

    private void setNameValue(String name, String value) {
        if (PATH_DESCRIPTION.equals(name)) {
            mNodeList = PathParser.parsePath(value);
        } else if (PATH_ID.equals(name)) {
            mName = value;
        } else if (PATH_FILL.equals(name)) {
            mFillColor = calculateColor(value);
        } else if (PATH_FILL_TYPE.equals(name)) {
            mFillType = parseFillType(value);
        } else if (PATH_STROKE.equals(name)) {
            mStrokeColor = calculateColor(value);
        } else if (PATH_FILL_OPACITY.equals(name)) {
            mFillAlpha = Float.parseFloat(value);
        } else if (PATH_STROKE_OPACTIY.equals(name)) {
            mStrokeAlpha = Float.parseFloat(value);
        } else if (PATH_STROKE_WIDTH.equals(name)) {
            mStrokeWidth = Float.parseFloat(value);
        } else if (PATH_TRIM_START.equals(name)) {
            mTrimPathStart = Float.parseFloat(value);
        } else if (PATH_TRIM_END.equals(name)) {
            mTrimPathEnd = Float.parseFloat(value);
        } else if (PATH_TRIM_OFFSET.equals(name)) {
            mTrimPathOffset = Float.parseFloat(value);
        } else if (PATH_STROKE_LINECAP.equals(name)) {
            if (LINECAP_BUTT.equals(value)) {
                mStrokeLineCap = 0;
            } else if (LINECAP_ROUND.equals(value)) {
                mStrokeLineCap = 1;
            } else if (LINECAP_SQUARE.equals(value)) {
                mStrokeLineCap = 2;
            }
        } else if (PATH_STROKE_LINEJOIN.equals(name)) {
            if (LINEJOIN_MITER.equals(value)) {
                mStrokeLineJoin = 0;
            } else if (LINEJOIN_ROUND.equals(value)) {
                mStrokeLineJoin = 1;
            } else if (LINEJOIN_BEVEL.equals(value)) {
                mStrokeLineJoin = 2;
            }
        } else if (PATH_STROKE_MITERLIMIT.equals(name)) {
            mStrokeMiterlimit = Float.parseFloat(value);
        } else {
            LOGGER.log(Level.WARNING, ">>>>>> DID NOT UNDERSTAND ! \"" + name + "\" <<<<");
        }

    }

    private static int parseFillType(String value) {
        if (FILL_TYPE_EVEN_ODD.equalsIgnoreCase(value)) {
            return PathIterator.WIND_EVEN_ODD;
        }
        return PathIterator.WIND_NON_ZERO;
    }

    /**
     * Multiply the <code>alpha</code> value into the alpha channel <code>color</code>.
     */
    private static int applyAlpha(int color, float alpha) {
        int alphaBytes = (color >> 24) & 0xff;
        color &= 0x00FFFFFF;
        color |= ((int) (alphaBytes * alpha)) << 24;
        return color;
    }

    /**
     * Draw the current path
     */
    @Override
    public void draw(Graphics2D g, AffineTransform currentMatrix, float scaleX, float scaleY) {

        Path2D path2d = new Path2D.Double(mFillType);
        toPath(path2d);

        // SWing operate the matrix is using pre-concatenate by default.
        // Below is how this is handled in Android framework.
        // pathMatrix.set(groupStackedMatrix);
        // pathMatrix.postScale(scaleX, scaleY);
        g.setTransform(new AffineTransform());
        g.scale(scaleX, scaleY);
        g.transform(currentMatrix);

        // TODO: support clip path here.
        if (mFillColor != 0) {
            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            Color fillColor = new Color(applyAlpha(mFillColor, mFillAlpha), true);
            g.setColor(fillColor);
            g.fill(path2d);
        }
        if (mStrokeColor != 0) {
            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            BasicStroke stroke = new BasicStroke(mStrokeWidth, mStrokeLineCap, mStrokeLineJoin, mStrokeMiterlimit);
            g.setStroke(stroke);
            Color strokeColor = new Color(applyAlpha(mStrokeColor, mStrokeAlpha), true);
            g.setColor(strokeColor);
            g.draw(path2d);
        }
    }

    @Override
    public void parseAttributes(NamedNodeMap attributes) {
        int len = attributes.getLength();
        for (int i = 0; i < len; i++) {
            String name = attributes.item(i).getNodeName();
            String value = attributes.item(i).getNodeValue();
            setNameValue(name, value);
        }
    }

    @Override
    public boolean isGroup() {
        return false;
    }

    @Override
    public String toString() {
        //noinspection ImplicitArrayToString
        return "Path:" +
                " Name: " + mName +
                " Node: " + mNodeList.toString() +
                " mFillColor: " + Integer.toHexString(mFillColor) +
                " mFillAlpha:" + mFillAlpha +
                " mFillType:" + mFillType +
                " mStrokeColor:" + Integer.toHexString(mStrokeColor) +
                " mStrokeWidth:" + mStrokeWidth +
                " mStrokeAlpha:" + mStrokeAlpha;
    }
}
